# Redis 简介

  • 高性能的 key-value 内存数据库(Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s),用 C 语言编写
  • Redis 高性能的三个因素:纯内存存储;IO 多路复用技术;单线程架构
  • 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
  • Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作全并后的原子性执行
  • 支持 publish/subscribe、通知、key 过期等等特性
  • Redis 的键值支持的数据类型:string(字符串),hash(哈希),list(列表),set(集合)及 zset(有序集合)、BitMap(位图)、HyperLogLog、Geo(地理位置)
  • Redis 内置了复制(Replication),LUA 脚本(Lua scripting),LRU 驱动事件(LRU eviction),事务(Transactions)和不同级别的持久化(Persistence),并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)
  • Redis 使用场景:缓存、排行榜、计数器、社交网络(赞/踩、粉丝、共同好友/喜好等)、消息队列
  • 配置文件 (opens new window)

# Redis 的单线程

  • Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的
  • Redis 的其他功能,比如持久化、异步删除、集群数据同步等,是由额外的线程执行的
  • Redis 采用单线程模式处理请求,原因:
    1. 采用了非阻塞的 I/O 多路复用机制
    2. 缓存数据都是内存操作,IO 时间不会太长,单线程可以避免不必要的上下文切换和竞争条件
  • Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽
  • 单线程带来的问题:如果某个命令执行过长,会造成其它命令的阻塞

# Reids 工具命令

  • redis-server:Redis 服务器的 daemon 启动程序
  • redis-cli:Redis 命令行操作工具,如连接 Redis redis-cli -h host -p port -a password
  • redis-benchmark:Redis 性能测试工具,如模拟同时由 50 个客户端发送 100000 个 SETs/GETs 查询 redis-benchmark -n 100000 -c 50
  • redis-check-aof:更新日志检查
  • redis-check-dump:本地数据库检查

# Redis 常用命令 (opens new window)

# Redis 常用管理命令

# Redis 连接

  • auth hello:验证密码 hello 是否正确(需修改配置项:requirepass hello)
  • ping:查看服务是否运行(Redis 的默认监听端口为 6379)
  • echo message:打印字符串
  • ping:查看服务是否运行
  • quit:关闭当前连接
  • client id:返回当前连接的 ID(每个连接 ID 永不重复且单调递增)
  • client setname hello-world-connection:设置连接的名称
  • client getname:获取连接的名称
  • select index:切换到指定的数据库(默认 16 个库,默认使用 0 号数据库)

# Redis 服务器

  • client list:获取连接到服务器的客户端连接列表
  • client kill [ip:port] [id client-id]:关闭客户端连接
  • info [server|clients|memory|persistence|stats|replication|cpu|commandstats|cluster|keyspace]:返回关于 Redis 服务器的各种信息和统计数值,section 可选值
  • config get parameter:获取一个 Redis 配置参数信息,如查看是否设置了密码验证 config get requirepass
  • config set parameter value:设置一个 Redis 配置参数信息,如设置密码 config set requirepass "hello"
  • config resetstat:重置 info 命令的统计信息,包括 keyspace 命中数、keyspace 错误数、处理命令数,接收连接数、过期 key 数 等
  • monitor:实时监听并返回 Redis 服务器接收到的所有请求信息
  • flushall:删除所有数据库的所有 key
  • flushdb:删除当前数据库的所有 key
  • debug segfault:制造一次服务器宕机
  • save:异步保存数据到硬盘
  • shutdown:把数据同步保存到磁盘上,并关闭 Redis 服务

# Key(键)

  • keys pattern:查找所有符合给定模式(pattern)的 key
  • dbsize:返回当前数据库 key 的数量
  • exists key:检查给定 key 是否存在,如果存在则返回 1, 不存在则返回 0
  • del key:在 key 存在时删除 key,返回被删除 key 的数量
  • expire key seconds:key 在 seconds 秒后过期
  • pexpire key milliseconds:key 在 milliseconds 毫秒后过期
  • expireat key timestamp:key 在秒级时间戳 timestamp 后过期
  • pexpireat key milliseconds-timestamp:key 在毫秒级时间戳 milliseconds-timestamp 后过期
  • ttl key:以秒为单位返回 key 的剩余生存时间(time to live),当 key 不存在时,返回 -2,当 key 存在但没有设置剩余生存时间时,返回 -1
  • pttl key
  • persist key:将 key 的过期时间清除
  • rename key newkey:修改 key 的名称
  • renamenx key newkey:仅当 newkey 不存在时,将 key 改名为 newkey
  • type key:返回 key 所储存的值的类型
  • debug object key:返回 key 的调试信息
  • move key db:将当前数据库的 key 移动到给定的数据库 db 当中
  • scan cursor [match pattern] [count number]:采用渐进式遍历当前数据库中的数据库键,cursor 是一个游标,第一次遍历从 0 开始,每次 scan 遍历完都会返回当前游标的值(相关命令:sscan、hscan、zscan)
  • 键名:业务名:对象名:id:[属性]
  • 迁移键:migrate 命令

Redis的5种数据结构

# String(字符串)

  • 可以存储字符串、整数、浮点数、二进制,值最大不能超过 512MB

  • set key value [ex seconds] [px milliseconds] [nx|xx]:设置指定 key 的值

  • setnx key value:只有在 key 不存在时设置 key 的值,设置成功返回 1,设置失败返回 0

  • setex key seconds value:为指定的 key 设置值及其过期时间,如果 key 已经存在,将会替换已有的值

  • mset key value [key value ...]:批量设置值

  • get key:获取指定 key 的值,如果要获取的键不存在, 则返回 nil

  • mget key [key ...]:批量获取值

  • getset key value:将给定 key 的值设为 value,并返回 key 的旧值

  • getrange key start end:返回 key 中字符串值的子字符

  • incr key:将 key 中储存的数字值增一

  • incrby key increment:将 key 所储存的值加上给定的增量值(increment)

  • incrbyfloat key increment:将 key 所储存的值加上给定的浮点增量值(increment)

  • decr key:将 key 中储存的数字值减一

  • decrby key decrement:key 所储存的值减去给定的减量值(decrement)

  • strlen key:返回 key 所储存的字符串值的长度

  • append key value:如果 key 已经存在并且是一个字符串,append 命令将 value 追加到 key 原来的值的末尾

  • 使用场景:缓存(存储 JSON 格式的对象)、计数(优酷视频点赞)、分布式锁、共享 Session、限速(短信验证码限速)

# Hash(哈希、字典)

  • 键值对集合,一个 String 类型的 field 和 value 的映射表
  • hset key field value:将哈希表 key 中的 field 的值设为 value
  • hget key field:获取存储在哈希表中指定 field 的值
  • hmset key field1 value1 [field2 value2]:同时将多个 field-value 对设置到哈希表 key 中
  • hmget key field1 [field2]:获取所有给定 field 的值
  • hdel key field2 [field2]:删除哈希表中一个或多个 field
  • hexists key field:查看哈希表 key 中指定的 field 是否存在
  • hincrby key field increment:为哈希表 key 中的指定 field 的整数值加上增量 increment
  • hlen key:获取哈希表中 field 的个数
  • hkeys key:获取哈希表中所有的 field
  • hvals key:获取哈希表中所有的 value
  • hgetall key:获取哈希表中所有的 field 和 value
  • hscan key cursor [match pattern] [count count]:迭代哈希键中的键值对,第一次遍历 cursor 从 0 开始
  • 使用场景:存储、读取、修改对象属性

# List(列表)

  • 双向链表结构,按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边),一个列表最多可以存储 232-1 个元素

  • 对列表两端插入(push)和弹出(pop),获取指定范围的元素列表、获取指定索引下标的元素等

  • rpush key value1 [value2]:从右边插入元素

  • lpush key value1 [value2]: 从左边插入元素

  • linsert key before|after pivot value:向某个元素前或者后插入元素

  • lpop key:从列表左侧弹出元素

  • rpop key:从列表右侧弹出

  • lindex key index:获取列表指定索引下标的元素( 索引从 0 开始)

  • lrange key start stop:获取列表指定范围内的元素

  • llen key:获取列表长度

  • lrem key count value:删除列表元素

  • ltrim key start stop:对一个列表进行修剪,让列表只保留指定区间内的元素

  • lset key index newValue:修改指定索引下标的元素

  • blpop key [key ...] timeout:阻塞式左侧弹出,当给定列表内没有任何元素可供弹出的时候,连接将被阻塞,直到等待超时或发现可弹出元素为止,timeout 为阻塞时间(单位:秒),如果 timeout=0,那么客户端一直阻塞等下去

  • brpop key [key ...] timeout:阻塞式右侧弹出

  • 使用场景:时间轴(微博 TimeLine)、消息队列(回帖、聊天记录、文章评论)、朋友圈点赞

# Set(集合)

  • 哈希表结构,无序集合,且成员不允许重复,一个集合最多可以存储 232-1 个元素
  • 支持集合内的增删改查,同时还支持多个集合取交集、并集、差集
  • sadd key member1 [member2]:向集合添加一个或多个成员
  • srem key member1 [member2]:移除集合中一个或多个成员
  • scard key:获取集合的成员数
  • sismember key member:判断 member 元素是否是集合 key 的成员
  • smembers key:返回集合中的所有成员
  • spop key:移除并返回集合中的一个随机元素
  • srandmember key [count]:返回集合中一个或多个随机数
  • sunion key1 [key2]:返回所有给定集合的并集
  • sunionstore destination key1 [key2]:所有给定集合的并集存储在 destination 集合中
  • sinter key1 [key2]:返回给定所有集合的交集
  • sinterstore destination key1 [key2]:返回给定所有集合的交集并存储在 destination 中
  • sdiff key1 [key2]:返回给定所有集合的差集
  • sdiffstore destination key1 [key2]:返回给定所有集合的差集并存储在 destination 中
  • sscan key cursor [match pattern] [count count]:迭代集合键中的元素,第一次遍历 cursor 从 0 开始
  • 使用场景:去重(统计访问网站的所有独立 IP)、生成随机数(抽奖)、共同好友(求交集)、好友推荐(求并集后再求差集)

# Sorted Set(有序集合)

  • 跳跃表结构,有序集合,且不允许重复的成员
  • 每个元素都关联一个 double 类型的分数,通过分数来为集合中的成员进行从小到大的排序(在分数相同的情况下,对成员按照二进制大小进行顺序)
  • 有序集合的成员是唯一的,但分数(score)可以重复
  • zadd key score1 member1 [score2 member2]:向有序集合添加一个或多个成员,或者更新已存在成员的分数
  • zrem key member [member ...]:移除有序集合中的一个或多个成员
  • zcard key:获取有序集合的成员数
  • zcount key min max:计算在有序集合中指定区间分数的成员数
  • zrange key start stop [withscores]:通过索引区间返回有序集合成指定区间内的成员
  • zrevrange key start stop [withscores]:返回有序集中指定区间内的成员,通过索引,分数从高到底
  • zscore key member:返回有序集合中指定成员的分数
  • zrank key member:返回有序集合中指定成员的索引
  • zrevrank key member:返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
  • zincrby key increment member:有序集合中对指定成员的分数加上增量 increment
  • zinterstore destination numkeys key [key ...]:计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中
  • zscan key cursor [match pattern] [count count]:迭代有序集合中的元素(包括元素成员和元素分值),第一次遍历 cursor 从 0 开始
  • zpopmin key [count]:Remove and return members with the lowest scores in a sorted set
  • zpopmax key [count]:Remove and return members with the highest scores in a sorted set
  • bzpopmin key [key ...] timeout:Remove and return the member with the lowest score from one or more sorted sets, or block until one is available
  • bzpopmax key [key ...] timeout:Remove and return the member with the highest score from one or more sorted sets, or block until one is available
  • 使用场景
    带有权重的元素,比如一个游戏的用户得分排行榜
    缓存淘汰算法:LRU(Least recently used 最近最少使用)、LFU(Least Frequently Used 最不经常使用)

# Bitmaps(位图)

  • 一个由二进制位组成的数组,数组的下标在 Bitmaps 中叫做偏移量
  • 可以对字符串的位进行操作
  • Bitmaps 实际类型为字符串类型(不能超过 512MB),所以 offset 范围为 0~232
  • setbit key offset 1|0:设置或清除指定偏移量上的 bit 值(offset 从 0 开始)
  • getbit key offset:获取指定偏移量上的 bit 值
  • bitcount key [start end]:统计位图指定起止位置的值为“1”比特(bit)的位数
  • bitop and|or|not|xor destkey key [key ... ]:对一个或多个位图进行比特位运算操作,并将结果保存在 destkey 中
  • bitpos key 1|0 [start] [end]:获取位图中第一个 bit 值为 1或 0 的位置,可指定要检测的起止位置
  • 使用场景:统计网站独立访问用户(偏移量作为用户的 id)

# HyperLogLog

  • HyperLogLog 是一种基数算法( 实际类型为字符串类型),可以对集合的基数进行估算(一个带有 0.81% 标准错误的近似值)
  • HyperLogLog 只能根据输入元素来计算基数,而不能存储输入元素本身
  • 每个 HyperLogLog 只占 12KB 内存
  • pfadd key element [element …]:向 HyperLogLog 添加元素
  • pfcount key [key …]:返回 HyperLogLog(或多个 HyperLogLog 的并集)的近似基数
  • pfmerge destkey sourcekey [sourcekey .. . ]:将多个 HyperLogLog 合并为一个 HyperLogLog,并赋值给destkey
  • 使用场景:统计 UV

# Geo(地理位置)

  • Geo 的数据类型为 zset,Redis 将所有地理位置信息的 geohash 存放在 zset 中

  • geoadd key longitude latitude member [longitude latitude member … ]:将指定的空间元素添加到指定的 key 里

  • geopos key member [member … ]:返回地理位置的经纬度

  • geohash key member [member … ]:返回地理位置的 geohash 字符串(geohash 将二维经纬度转换为一维字符串)

  • geodist key member1 member2 [unit]:返回两个地理位置之间的距离,unit 为返回位置之间距离的单位,可以是 m(米)、km(千米)、mi(英里)、ft(英尺),默认为 m

  • georadius key longitude latitude radius m|km|mi|ft:以给定的经纬度为中心,查询询指定半径内所有的地理位置元素的集合

  • georadiusbymember key member radius m|km|mi|ft:以给定的位置元素为中心,查询指定半径内所有的地理位置元素的集合

  • zrem key member:删除地理位置元素

# PUB/SUB(发布订阅)

  • 由发布者发布信息,存储到指定的频道上,然后订阅者根据自己订阅的频道接收信息
  • 目前 Redis 的发布订阅功能采取的是发送即忘(fire and forget)策略,不会对发布的消息进行持久化,因此新加入的订阅者,无法收到该频道之前的消息;当订阅事件的客户端断线时,会丢失所有在断线期间分发给它的事件

Redis发布订阅模型

  • publish channel message:发布信息到指定的频道,返回订阅者个数
  • subscribe channel [channel . . . ]:订阅指定频道
  • unsubscribe [channel [channel ... ]]:取消对指定频道的订阅
  • psubscribe pattern [pattern …]:订阅符合给定模式的信息
  • punsubscribe [pattern [pattern …]]:取消对所有给定模式信息的订阅
  • pubsub channels [pattern]:查看当前的活跃频道(至少有一个订阅者的频道) ,订阅模式的客户端不计算在内
  • pubsub numsub [channel ...]:查看频道订阅数
  • pubsub numpat:查看模式订阅数
  • 键空间通知 (opens new window)Keyspace notifications (opens new window)):使客户端可以通过订阅频道或模式, 来接收那些以某种方式改动了 Redis 数据集的事件
    • 在默认配置下, 该功能处于关闭状态(开启键空间通知功能需要消耗一些 CPU)
    • 对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件:键空间通知(以 __keyspace@<db>__ 为前缀)、键事件通知(以 __keyevent@<db>__ 为前缀)
    • 注意:过期事件是在 Redis 服务器删除键时生成的,而不是在理论上生存时间达到零值时生成

# Streams (opens new window)

  • xadd
  • xrange
  • xread

# 事务

  • 通常的命令组合:watch ... multi ... exec
  • 将一组需要一起执行的命令放到 multi 和 exec 两个命令之间(multi 命令代表事务开始,exec 命令代表事务结束)
  • 事务中的命令出现语法错误,会造成整个事务无法执行
  • 事务中的命令在运行时,如果遇到某条命令执行错误,错误后面的其它命令依旧被执行成功,因为 Redis 不支持回滚功能
  • multi:标记一个事务块的开始,开始事务后,该客户端的命令不会马上被执行,而是存放在一个队列里,如果在这时执行一些返回数据的命令,结果都是返回 null
  • exec:执行所有事务块内的命令,事务块内所有的命令都被执行时,返回所有命令执行的返回值;若事务被打断,返回 nil
  • discard:取消事务,放弃执行事务块内的所有命令(在取消事务的同时,也会取消对 key 的监视)
  • watch key [key …]:监视一个或多个 key,如果在事务执行之前,这个(或这些)key 被其它命令所改动,那么事务将被打断(类似乐观锁)
  • unwatch:取消 watch 命令对所有 key 的监视

Redis事务执行过程

# Lua 脚本

在 Redis 中有两种运行 Lua 的方法,一种是直接发送 Lua 到 Redis 服务器去执行,另一种是先把 Lua 发送给 Redis,Redis 会对 Lua 脚本进行缓存,然后返回一个 SHA1 的 32 位编码,之后只需要发送 SHA1 和相关参数给 Redis 即可执行。

# Redis 内存

# Redis 内存回收策略

  • 删除过期键对象/过期机制

    1. 惰性删除:主节点每次处理读取命令时,都会检查键是否超时,如果超时则执行 del 命令删除键对象,之后 del 命令也会异步发送给从节点
    2. 定时任务删除:Redis 主节点在内部定时任务会循环采样一定数量的键,当发现采样的键过期时执行 del 命令,之后再同步给从节点
  • 内存溢出控制策略/内存淘汰策略
    当 Redis 所用内存达到 maxmemory 上限时会触发相应的溢出控制策略(maxmemory-policy),Redis 支持 6 种策略:

    1. noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息
    2. allkeys-lru:针对所有 key,优先删除最近最少使用的 key,直到腾出足够空间为止
    3. volatile-lru:针对带有过期时间的 key,优先删除最近最少使用的 key,直到腾出足够空间为止
    4. allkeys-random:随机删除所有 key,直到腾出足够空间为止
    5. volatile-random:随机删除带有过期时间的 key,直到腾出足够空间为止
    6. volatile-ttl:针对带有过期时间的 key,优先删除即将过期的 key(根据 TTL 的值)
    7. allkeys-lfu:(Redis 4.0 以上)针对所有 key,优先删除最少使用的 key
    8. volatile-lfu:(Redis 4.0 以上)针对带有过期时间的 key,优先删除最少使用的 key

    The policies volatile-lru, volatile-random and volatile-ttl behave like noeviction if there are no keys to evict matching the prerequisites.(即如果没有可删除的键对象,回退到 noeviction 策略)

  • 常见的缓存淘汰策略:

    • 先进先出策略 FIFO(First In First Out):淘汰最先进入缓存的数据
    • 最不经常使用策略 LFU(Least Frequently Used):淘汰被访问次数最少的数据,以次数作为参考
    • 最近最少使用策略 LRU(Least Recently Used):淘汰最长时间未被使用的数据,以时间作为参考

    LFU、LRU

# 内存配置优化

hash、list、set、zset内部编码配置

  • Redis 的字符串结构:简单动态字符串(simple dynamic string,SDS),SDS 类似于 Java 的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配
  • ziplist 压缩编码的性能表现跟值长度和元素个数密切相关
  • 使用 ziplist 编码时,建议长度不要超过 1000,每个元素大小控制在 512 字节以内
  • 使用 intset 编码的集合时,尽量保持整数范围一致,如都在 int-16 范围内

# Redis 相关问题及解决方案

  • 对于缓存瞬时大面积失效的缓存雪崩问题,可以通过差异化缓存过期时间解决
  • 对于高并发的缓存 key 回源问题,可以使用锁来限制回源并发数
  • 对于不存在的数据穿透缓存的问题,可以通过布隆过滤器进行数据存在性的预判,或在缓存中也设置一个值来解决
  • “先更新数据库再删除缓存,访问的时候按需加载数据到缓存”的策略是最为妥当的

# 缓存雪崩

  • 发生场景:短时间内大量缓存失效,导致在瞬间有大量的数据需要回源到数据库查询,对数据库造成极大的压力,极限情况下甚至导致后端数据库直接崩溃
  • 发生原因:1. 缓存系统本身不可用;2. 应用设计层面大量的 key 在同一时间过期
  • 解决方案(针对原因二):
    1. 为 key 设置不同的过期时间,如在原有的失效时间基础上增加一个 1-5 分钟随机值
    2. 让缓存永不过期,定时更新缓存
    3. 缓存预热:在上线时先将需要缓存的数据放到缓存中去

# 缓存击穿

  • 发生场景:在某些 Key 属于极端热点数据,且并发量很大的情况下,如果这个 Key 过期,可能会在某个瞬间出现大量的并发请求同时回源,相当于大量的并发请求直接打到了数据库

  • 解决方案:

    1. 让缓存永不过期
    2. 定时去更新快过期的缓存
    3. 使用互斥锁(mutex key)或 Semaphore 限制回源的并发数

# 缓存穿透

  • 发生场景:要查询的数据不存在,缓存无法命中所以需要查询数据库,如果从数据库查不到数据则不写入缓存,这将导致每次对该数据的查询都会去数据库查询

  • 解决方案:

    1. 缓存空值:即使这条数据不存在也将其存储到缓存中去,设置一个较短的过期时间,并且可以做日志记录,寻找问题原因
    2. 布隆过滤器拦截:预先将所有的 key 用布隆过滤器保存起来,然后过滤掉那些不存在的 key

# 布隆过滤器(Bloom Filter)

  • 一种概率型数据库结构,用于检索一个元素是否在一个集合中,优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误判率和删除困难
  • 布隆过滤器实际上是一个 bit 向量或者说 bit 数组,使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值对应的 bit 位置 1
  • 检索时,如果这些 bit 位有任何一个 0,则被检元素一定不存在;如果都是 1,则被检元素很可能存在
  • 核心思想
    1. 使用多个哈希函数,增大随机性,减少 hash 碰撞的概率
    2. 扩大数组范围,使 hash 值均匀分布,进一步减少 hash 碰撞的概率
  • Bloom Filter Calculator (opens new window)
  • com.google.common.hash.BloomFilter

# 缓存预热

  • 系统上线后,将相关的缓存数据直接加载到缓存系统,避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题

  • 实现方法:在项目启动的时候自动进行加载;写一个隐秘的接口用于加载缓存

# 缓存更新策略

  1. 定时去更新快过期的缓存
  2. 主动更新
    • 先更新数据库再删除缓存,等访问时再加载数据到缓存
      • 在极端情况下,这种策略也可能出现数据不一致。例如更新数据的时间节点恰好是缓存失效的瞬间,这时 A 先读取到了旧值,随后在 B 操作数据库完成更新并且删除了缓存之后,A 再把旧值加入缓存。解决方法:延时双删,即等 2s(读业务逻辑数据的耗时 + 几百毫秒) 后再删除一次缓存。
      • 如果更新数据库后删除缓存失败,可以把失败任务加入延时队列进行延迟重试,确保缓存数据可以被删除。(保证数据的最终一致性)
    • 先更新数据库再利用消息系统或者其它方式通知缓存更新
    • 读取 biglog 异步删除缓存

# 处理 bigkey

  • bigkey 是指 key 对应的 value 所占的内存空间比较大,如字符串类型单个 value 值超过 10KB,非字符串类型的元素个数超过 5000
  • bigkey 的危害
    1. 在 Redis Cluster 中,bigkey 会造成节点的内存空间使用不均匀
    2. 操作 bigkey 比较耗时,容易阻塞 Redis
    3. 每次获取 bigkey 产生的网络流量较大,容易造成网络拥塞
  • 查询 bigkey
    1. redis-cli --bigkeys,key 比较多时执行该命令会比较慢
    2. 获取生产 Redis 的 rdb 文件,通过 rdbtools 分析 rdb 生成 csv 文件,再导入 MySQL 或其他数据库中进行分析统计,根据 size_in_bytes 统计 bigkey
  • 优化 bigkey 的原则:string 减少字符串长度,hash、list、set、zset 等减少元素个数
  • 解决方案:
    1. 拆分数据结构
    2. 取值时不要把所有元素都取出来,如使用 hmget
    3. 将该 key 迁移到一个新的 Redis 节点上

# 处理 hotkey

  • hotkey 的危害:当有大量的请求(几十万)访问某个 Redis 某个 key 时,由于流量集中达到网络上限,从而导致 Redis 宕机,造成缓存击穿,接下来对这个 key 的访问将直接访问数据库造成数据库崩溃,或者访问数据库回填 Redis 再访问 Redis,继续崩溃。
  • 发现 hotkey
    1. 预估热 key,比如秒杀的商品、火爆的新闻等
    2. redis-cli --hotkeys,内存溢出控制策略需设置为 LFU,key 比较多时执行该命令会比较慢
    3. 利用基于大数据领域的流式计算技术来进行实时统计数据的访问次数,比如 Storm、Spark、Streaming、Flink 等,发现热点数据后可以写到 ZooKeeper 中
  • 处理 hotkey
    1. 将热点数据自动加载为 JVM 本地缓存(可能存在缓存不一致)
    2. 对热点数据访问进行限流熔断保护

# 持久化

  • RDB 方式(默认):通过快照(snapshotting)完成,当符合一定条件(在指定的时间内被更改的键的个数大于指定的数值)时 Redis 会自动将内存中的所有数据生成快照并存储在 dump.rdb 文件(默认)中,也可以使用 bgsave 命令手动触发
  • AOF(append only file)方式:每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令以追加的方式写入硬盘中的 AOF 文件;定期对 AOF 文件进行重写(把 Redis 进程内的数据转化为写命令同步到新 AOF 文件)
  • 比较:RDB 方式无法做到数据实时持久化/秒级持久化;Redis 加载 RDB 恢复数据远远快于 AOF 方式
  • AOF 持久化开启且存在 AOF 文件时,优先加载 AOF 文件,否则加载 RDB 文件

# 主从复制 + 哨兵(Sentinel)

# 主从复制

  • 主节点负责写数据,从节点负责读数据(slave 支持只读模式且默认开启)

  • 复制过程

    1. 从节点与主节点建立 socket 连接,从节点发送 ping 命令,主节点权限验证(如果主节点设置了 requirepass 参数)
    2. 从节点向主节点发送 psync 命令,执行同步操作:
      • 全量复制(full resynchronization):如果是第一次进行复制,主节点执行 bgsave 保存 RDB 文件到本地,发送 RDB 文件给从节点,从节点把接收的 RDB 文件保存在本地,然后清空自身旧数据开始加载 RDB 文件;对于从节点开始接收 RDB 快照到接收完成期间,主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完 RDB 文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性
      • 部分复制(partial resynchronization):当主从节点之间网络出现中断,从节点再次连上主节点后,从节点会向主节点要求补发丢失的命令数据,主节点根据偏移量复制积压缓冲区里的数据发送给从节点
    3. 异步复制:主节点持续地把写命令发送给从节点,保证主从数据一致性

# 哨兵架构

Redis_Sentinel架构

  1. Redis Sentinel 是一个分布式架构,其中包含若干个(2n+1,至少 3 个)哨兵节点和 Redis 数据节点
  2. 主从复制节点是数据节点,哨兵机制部署的节点是监控节点,它们都是 Redis 实例,但是哨兵节点不存储数据
  3. 每个哨兵节点会对数据节点和其余哨兵节点进行监控,当它发现某个节点不可达时,会对节点做下线标识,如果被标识的是主节点,它还会和其它哨兵节点进行“协商
  4. 超过半数哨兵节点都认为主节点不可达时,它们会选举出一个哨兵节点来完成自动故障转移的工作,同时会将这个变化实时通知给 Redis 客户端

# 集群(Cluster)

  • 可以使用 redis-trib.rb 工具搭建集群
  • 节点通信:集群内部节点通信采用 Gossip(流言)协议彼此发送消息,节点定期不断发送和接受 ping/pong 消息来维护更新集群的状态(集群中的每个节点都会单独开辟一个 TCP 通道,用于节点之间彼此通信,通信端口号在基础端口上加 10000)
  • Redis 集群采用的数据分区规则是 Hash Slots(哈希虚拟槽分区):Redis 集群把所有的数据映射到 16384 个槽中,每个节点负责一定数量的槽,即先对每个 key 计算 CRC16 值,再对 16384 取模,将 key 映射到一个编号在 0~16383 之间的槽内(slot = CRC16(key) % 16384),然后把键值对存放到对应范围的节点上
  • 集群功能限制:
    • 不支持在多个节点上执行 mset、mget 等批量操作
    • 在不同的节点上无法多个 key 使用事务功能
    • 数据库只能使用 db0
  • 集群伸缩通过在节点之间移动槽和相关数据实现
  • 请求路由
    • 节点接收到键命令时会判断相关的槽是否由自身节点负责,如果不是则返回重定向信息(重定向信息包含了键所对应的槽以及负责该槽的节点地址)
    • 使用 Smart 客户端操作集群时,客户端内部负责计算维护 键→槽→节点 的映射,用于快速定位键命令到目标节点
    • 重定向分为 MOVED 和 ASK:ASK 说明集群正在进行槽数据迁移,客户端只在本次请求中做临时重定向,不会更新本地槽缓存;MOVED 重定向说明槽已经明确分派到另一个节点,客户端需要更新槽节点缓存
  • 集群自动故障转移

6节点Redis集群连接示意图

# Redis 的 Java client

  • Jedis:A blazingly small and sane redis java client. (Jedis 在实现上是直接连接 Redis-Server,在多个线程间共享一个 Jedis 实例时是线程不安全的,如果想要在多线程场景下使用 Jedis,需要使用连接池,每个线程都使用自己的 Jedis 实例,当连接数量增多时,会消耗较多的物理资源)
  • Lettuce:Advanced Redis client for thread-safe sync, async, and reactive usage. Supports Cluster, Sentinel, Pipelining, and codecs. (Lettuce 是一个可伸缩的线程安全的 Redis 客户端,支持同步、异步和响应式模式。多个线程可以共享一个连接实例,而不必担心多线程并发问题。它基于优秀 Netty NIO 框架构建,支持 Redis 的高级功能,如 Sentinel,集群,流水线,自动重新连接和 Redis 数据模型。)
  • Redisson (opens new window):distributed and scalable Java data structures on top of Redis server. Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务

# Jedis 客户端的使用

  • 通过 JedisPoolConfig 配置 JedisPool,程序退出时需要通过 addShutdownHook 来关闭 JedisPool
  • Jedis 不是线程安全的,需要每次从 JedisPool 先获取到一个 Jedis,然后再调用 Jedis 的 API
  • JedisSentinelPool、JedisCluster
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
JedisPool jedisPool = new JedisPool(jedisPoolConfig(), host);
try (Jedis jedis = jedisPool.getResource()) {
    // Jedis API
}
1
2
3
4
5

# Spring 操作 Redis

# RedisTemplate

  • RedisTemplate 会自动从 RedisConnectionFactory 工厂中获取连接,然后执行对应的 Redis 命令,在最后会关闭 Redis 的连接

# 常用操作

// 获取字符串操作接口
redisTemplate.opsForValue();
// 获取散列操作接口
redisTemplate.opsForHash();
// 获取列表操作接口
redisTemplate.opsForList();
// 获取集合操作接口
redisTemplate.opsForSet();
// 获取有序集合操作接口
redisTemplate.opsForZSet();
// 获取基数操作接口
redisTemplate.opsForHyperLogLog();
// 获取地理位置操作接口
redisTemplate.opsForGeo();

// 使用 TypedTuple(默认实现类 DefaultTypedTuple)保存有序集合的元素

// 获取绑定键的操作类,可以对某个键的数据进行多次操作
// 获取字符串绑定键操作接口
redisTemplate.boundValueOps("string");
// 获取散列绑定键操作接口
redisTemplate.boundHashOps("hash");
// 获取列表(链表)绑定键操作接口
redisTemplate.boundListOps("list"};
// 获取集合绑定键操作接口
redisTemplate.boundSetOps("set");
// 获取有序集合绑定键操作接口
redisTemplate.boundZSetOps("zset");
// 获取地理位置绑定键操作接口
redisTemplate.boundGeoOps("geo");


// 向 redis 里存入数据并设置缓存时间
stringRedisTemplate.opsForValue().set("test", "100", 60 * 10, TimeUnit.SECONDS);

// 根据 key 获取缓存中的 val
stringRedisTemplate.opsForValue().get("test");

// val 做 -1 操作
stringRedisTemplate.boundValueOps("test").increment(-1);
stringRedisTemplate.opsForValue().increment("test", -1);

// val 做 +1 操作
stringRedisTemplate.boundValueOps("test").increment(1);

// 向指定 key 中存放 set 集合
stringRedisTemplate.opsForSet().add("red_123", "1", "2", "3");

// 根据 key 查看集合中是否存在指定数据
stringRedisTemplate.opsForSet().isMember("red_123", "1");

// 根据 key 获取 set 集合
stringRedisTemplate.opsForSet().members("red_123");

// 检查 key 是否存在,返回 boolean 值
stringRedisTemplate.hasKey("546545");

// 根据 key 删除缓存
stringRedisTemplate.delete("test");

// 设置过期时间
stringRedisTemplate.expire("red_123", 1000, TimeUnit.MILLISECONDS);

// 根据 key 获取过期时间
stringRedisTemplate.getExpire("test");

// 根据 key 获取过期时间并换算成指定单位
stringRedisTemplate.getExpire("test", TimeUnit.SECONDS);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# Redis 事务

// 使用 RedisCallback 回调接口,在同一条连接下执行多个 Redis 命令
redisTemplate.execute(new RedisCallback() {
    @Override
    public Object doInRedis(RedisConnection connection)
            throws DataAccessException {
        connection.set("key1".getBytes(), "value1".getBytes());
        connection.hSet("hash".getBytes(), "field".getBytes(), "hvalue".getBytes());
        return null;
    }
});

// 使用 SessionCallback 回调接口,在同一条连接下执行多个 Redis 命令(推荐),可用于实现事务
List<Object> txResults = redisTemplate.execute(new SessionCallback<List<Object>>() {
    @Override
    public List<Object> execute(RedisOperations operations) {
        // 设置要监控 key1
        operations.watch("key1");
        // 开启事务,在 exec 命令执行前,全部都只是进入队列
        operations.multi();
        operations.opsForValue().set("key2", "value2");
        // 获取值将为 null,因为 Redis 只是把命令放入队列
        Object value2 = operations.opsForValue().get("key2");
        // 执行 exec 命令,将先判别 key1 是否在监控后被修改过,如果是不执行事务,否则执行事务、
        // This will contain the results of all operations in the transaction
        return operations.exec();
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# @Transactional 支持

// 开启 @Transactional 事务支持,默认关闭,关闭时每个操作使用的是不同的连接
// Configures RedisTemplate to participate in transactions by binding connections to the current thread.
redisTemplate.setEnableTransactionSupport(true);

// 使用限制
// must be performed on thread-bound connection
redisTemplate.opsForValue().set("thing1", "thing2");
// read operation must be executed on a free (not transaction-aware) connection
redisTemplate.keys("*");
// returns null as values set within a transaction are not visible
redisTemplate.opsForValue().get("thing1");
1
2
3
4
5
6
7
8
9
10
11

By default, transaction Support is disabled and has to be explicitly enabled for each RedisTemplate in use by setting setEnableTransactionSupport(true). Doing so forces binding the current RedisConnection to the current Thread that is triggering MULTI. If the transaction finishes without errors, EXEC is called. Otherwise DISCARD is called. Once in MULTI, RedisConnection queues write operations. All readonly operations, such as KEYS, are piped to a fresh (non-thread-bound) RedisConnection.

https://docs.spring.io/spring-data/redis/docs/current/reference/html/

# Pipline(流水线)

List<Object> executePipelined(SessionCallback<?> session)

# Redis Repository

  • @EnableRedisRepositories
  • 实体注解:@RedisHash、@Id、@Indexed、@TimeToLive
  • 继承 CrudRepository

# 基于 Redis 实现的分布式锁 (opens new window)

// 加锁
// 当 key 不存在时进行 set 操作,若 key 已经存在则不做任何操作
// value 为 requestId,表示加锁的客户端
// 设置锁过期时间,防止死锁的产生
// 返回 true 表示加锁成功,返回 false 表示加锁失败
// SET resource_name my_random_value NX EX 30
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30L, TimeUnit.SECONDS);

// 释放锁
// 获取 key 的值,判断加锁的是不是当前客户端(即存储的值与指定的值相同),如果是再释放锁,即删除该 key
// Lua 脚本在 Redis 中是原子执行的
// 通过 execute 方法执行脚本:首先直接传该 Lua 脚本的 SHA1 值,如果在 Redis 中找不到缓存的 Lua 脚本导致报错,则在程序 catch 该错误,把整个脚本序列化后传入 Redis 进行执行
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// 返回 1 表示释放锁成功,返回 0 表示释放锁失败
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), Collections.singletonList(requestId));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 使用 Redisson (opens new window) 实现分布式锁:可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、信号量(Semaphore)、闭锁(CountDownLatch)

    Redisson 内部提供了一个监控锁的看门狗,它的作用是在 Redisson 实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是 30s,可以通过修改 Config.lockWatchdogTimeout 来另行指定,org.redisson.RedissonLock#renewExpiration

    RLock lock = redissonClient.getLock(lockName);
    lock.lock();
    try {
        // do something...
    } finally {
        lock.unlock();
    }
    
    1
    2
    3
    4
    5
    6
    7
Updated at: 2022-07-01 11:22:45