Redis 可以说是现在软件开发中最常用的中间件了,可以用来做 Cache、分布式锁、队列等用途,高级特性里面的 Geo、Bitmap、HyperLogLog 等更是具有"神奇"的能力,合理的应用可以大大提升我们开发系统的能力。同时 Redis 提供了完善的高可用方案,可以很好的支持 AP。但是 Redis 使用的不当也会造成一些问题,所以想要驯服 Redis,需要注意一些常见的问题。
基本数据类型
String
String 类型应该是我们最常用的数据类型,其底层实现是简单动态字符串 SDS(Simple Dynamic String)。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间,最大长度为 512M。
应用场景
#####字符串类型 当作为字符串类型时候经常用于缓存或者共享对象使用,比如 Session、分布式锁等。String 类型也是二进制安全的,也可以用来存放小文件。
整数类型
在 SET 的时候,如果值为整数值,那么 redisObject 的 encoding 则为 int(可以使用 OBJECT ENCODING key 命令来查看)。可以应用于秒杀、限流、计数等场景。
使用示例
字符串类型
ruby
## 普通字符串使用
127.0.0.1:6379> SET abc def
OK
127.0.0.1:6379> GET abc
"def"
## 锁使用使用
## 当已经存在 key 将返回 0
127.0.0.1:6379> SETNX abc edf
(integer) 0
127.0.0.1:6379> SETNX def def
(integer) 1
整数类型
ruby
## 整数类型使用
127.0.0.1:6379> SET abc 123
OK
## 原子加 1
127.0.0.1:6379> INCR abc
(integer) 124
### 原子减1
127.0.0.1:6379> DECR abc
(integer) 123
## 原子加 5
127.0.0.1:6379> INCRBY abc 5
(integer) 128
List
List 是简单的字符串列表,按照插入顺序排序。一个列表最多可以包含 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 32 − 1 2^{32}-1 </math>232−1 个元素(大约40亿)。
应用场景
List 根据其特性可以很容易实现队列、栈数据结构。当 LPUSH + RPOP 就实现了队列数据结构,而使用 LPUSH + LPOP 就可以实现栈数据结构。
BLPOP、BRPOP 会移出并获取列表的第一个或最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止,可以用来实现消息队列功能。
使用示例
ruby
## 队列右边加入元素
127.0.0.1:6379> RPUSH list 1 2
(integer) 2
## 获取队列中所有元素
127.0.0.1:6379> LRANGE list 0 -1
1) "1"
2) "2"
## 队列左边加入元素
127.0.0.1:6379> LPUSH list 0
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1
1) "0"
2) "1"
3) "2"
## 移除列表最后一个元素,并返回
127.0.0.1:6379> RPOP list
"2"
127.0.0.1:6379> LRANGE list 0 -1
1) "0"
2) "1"
## 移除列表第一个元素,并返回
127.0.0.1:6379> LPOP list
"0"
127.0.0.1:6379> LRANGE list 0 -1
1) "1"
## 使用 LTRIM 可以对列表进行裁剪,不在指定区间之内的元素都将被删除
127.0.0.1:6379> RPUSH list 2 3 4
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> LTRIM list 1 2
OK
127.0.0.1:6379> LRANGE list 0 -1
1) "2"
2) "3"
Hash
使用场景
Hash 也是最常用到的数据结构之一了,主要用来做数据归并。比如当缓存用户信息的时候,可以将用户ID、姓名、性别等其他信息,归并到同一个 key,在取用的时候也很方便,可以统一取出,也可以指定字段取出。
Hash 也支持 HSETNX 对指定字段设置值(当字段不存在时候),与普通的 SETNX 类似。但是 Hash 不支持字段级别过期时间,只能给整个 key 加过期时间。
使用示例
ruby
## 使用 HMSET 一次设置多个字段
127.0.0.1:6379> HMSET hash id 123 name jack sex man login_times 5
OK
127.0.0.1:6379> HGETALL hash
1) "name"
2) "jack"
3) "id"
4) "123"
5) "sex"
6) "man"
7) "login_times"
8) "5"
## Hash 也支持使用 HINCRBY 对字段进行加值
127.0.0.1:6379> HINCRBY hash login_times 1
(integer) 6
## Hash 也支持 HSETNX 对指定字段设置值(当字段不存在时候),与普通的 SETNX 类似
127.0.0.1:6379> HSETNX hash lock_key lock_value
(integer) 1
127.0.0.1:6379> HSETNX hash lock_key lock_value
(integer) 0
Set
Set 是 String 类型的去重无序集合,集合中成员是唯一的。
使用场景
Set 提供了随机移除、集合交集、集合并集、集合差集等操作。结合这些特性,可以用于如抽奖这样的随机事件,可以实现查找共同好友、可能认识的人、获取人脉等操作。
使用示例
ruby
## 向集合中添加对象
127.0.0.1:6379> SADD set1 a b c d
(integer) 4
127.0.0.1:6379> SADD set2 c d e f
(integer) 4
## 查看在 set1 没在 set2 的元素,差集
127.0.0.1:6379> SDIFF set1 set2
1) "a"
2) "b"
## 查看既在 set1 又在 set2 的元素,交集
127.0.0.1:6379> SINTER set1 set2
1) "c"
2) "d"
## 查看在 set1 或在 set2 的元素,并集
127.0.0.1:6379> SUNION set1 set2
1) "c"
2) "d"
3) "b"
4) "a"
5) "e"
6) "f"
## 随机获取指定数量的元素,不会从集合中删除元素
127.0.0.1:6379> SRANDMEMBER set1 2
1) "a"
2) "b"
127.0.0.1:6379> SRANDMEMBER set1 2
1) "a"
2) "d"
## 随机移除指定数量的元素,会从集合中删除元素
127.0.0.1:6379> SPOP set1 2
1) "b"
2) "c"
127.0.0.1:6379> SMEMBERS set1
1) "a"
2) "d"
## SDIFF、SINTER、SUNION 都支持后面加上 STORE,将计算结果存入指定的 key
127.0.0.1:6379> SDIFFSTORE set3 set1 set2
(integer) 1
127.0.0.1:6379> SMEMBERS set3
1) "a"
Sorted Set
Sorted Set 和 Set 一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数,通过分数来为集合中的成员进行从小到大的排序。
压缩表
在键值对少于 128 且每个元素小于 64 字节时候,使用压缩表(ziplist)数据结构来存储数据。数据结构如下:
- zlbytes:记录整个压缩列表占用的字节数
- zltail:记录压缩列表表尾节点距离压缩列表起始地址有多少字节
- zllen:记录了压缩列表包含的节点数量
- entryN:压缩列表的节点,节点长度由节点保存的内容决定
- zlend:特殊值 0xFF,用于标记压缩列表的末端
- previous_entry_length:记录了压缩表中前一个节点的长度
- encoding:记录了节点 content 属性所保存数据的类型以及长度,最两位标识类型,其余为长度
- content:保存节点的值,节点值可以是一个字节数组或者整数
跳表
在键值较多的时候将使用跳表(skiplist)数据结构来存储数据,跳表结构简单,结构如下所示。跳表与二叉查找树类似,都是为了提高查询效率,将 O(n) 的查询效率降低到 O(logn)。
使用场景
由于 Sorted Set 是一个有序集合,所以天然的适合进行排序相关的场景。如排行榜、具有优先级特性的队列功能等。
使用示例
ruby
## 向集合中添加元素
127.0.0.1:6379> ZADD sortedset 100 a 200 b 300 c 250 d
(integer) 4
127.0.0.1:6379> ZRANGE sortedset 0 -1 WITHSCORES
1) "a"
2) "100"
3) "b"
4) "200"
5) "d"
6) "250"
7) "c"
8) "300"
## 获取指定区间分数的成员数
127.0.0.1:6379> ZCOUNT sortedset 200 300
(integer) 3
## 对指定成员的分数加上增量值
127.0.0.1:6379> ZINCRBY sortedset 100 b
"300"
127.0.0.1:6379> ZRANGE sortedset 0 -1 WITHSCORES
1) "a"
2) "100"
3) "d"
4) "250"
5) "b"
6) "300"
7) "c"
8) "300"
## 返回集合中指定成员的排行,从 0 开始
127.0.0.1:6379> ZRANK sortedset d
(integer) 1
## 按照排行移除排行 0-1 的元素,利用此命令可以实现优先级队列
## ZREMRANGEBYSCORE 按照分数移除
## ZREMRANGEBYLEX 按照 key 字典区间移除
## ZREM 按照元素 key 移除
127.0.0.1:6379> ZREMRANGEBYRANK sortedset 0 1
(integer) 2
127.0.0.1:6379> ZRANGE sortedset 0 -1 WITHSCORES
1) "b"
2) "300"
3) "c"
4) "300"
高级特性
Geo
应用场景
在 Redis 3.2 版本中加入 Geo 相关功能,主要用于对地理数据的一些处理,使用 Geo 可以很方便的构建出周边查询功能。
使用示例
ruby
## 往指定 key 中增加带坐标的 Geo 元素
127.0.0.1:6379> GEOADD geo 116.397466 39.908628 tiananmen 116.411246 39.913171 wangfujing 116.44401 39.915557 ritan
(integer) 3
## 获取两个元素的距离,支持 m,km 等单位
127.0.0.1:6379> GEODIST geo tiananmen wangfujing m
"1279.6965"
## 获取指定坐标周边的点,可以指定返回的数量,及排序方式
127.0.0.1:6379> GEORADIUS geo 116.397466 39.908628 1300 m WITHDIST WITHCOORD COUNT 1000 ASC
1) 1) "tiananmen"
2) "0.0875"
3) 1) "116.39746695756912231"
2) "39.90862828249731109"
2) 1) "wangfujing"
2) "1279.7840"
3) 1) "116.41124814748764038"
2) "39.91317050281484313"
## 获取指定元素周边的点,与上面的命令类似
127.0.0.1:6379> GEORADIUSBYMEMBER geo wangfujing 3 km WITHDIST WITHCOORD
1) 1) "wangfujing"
2) "0.0000"
3) 1) "116.41124814748764038"
2) "39.91317050281484313"
2) 1) "ritan"
2) "2.8074"
3) 1) "116.44400864839553833"
2) "39.91555821014694772"
3) 1) "tiananmen"
2) "1.2797"
3) 1) "116.39746695756912231"
2) "39.90862828249731109"
Bitmap
应用场景
Bitmap 本质还是使用了 String 类型进行数据存储,使用 Bitmap 经常可以以很小的成本实现一些复杂的需求。如:统计任意时间窗口,用户登录次数,每个用户一个 365 位的 Bitmap,登录则天数位置为 1;预估活跃用户,按天存储用户的活跃情况,按照用户 ID 设置对应 bit 位;OA 系统的权限、Linux 的文件操作等。
使用示例
ruby
## 设置 key 指定 bit 位值
127.0.0.1:6379> SETBIT sign_in_week_12 5 1
(integer) 0
127.0.0.1:6379> SETBIT sign_in_week_12 1 1
(integer) 0
127.0.0.1:6379> SETBIT sign_in_week_12 3 1
(integer) 0
127.0.0.1:6379> SETBIT sign_in_week_12 4 1
(integer) 0
127.0.0.1:6379> SETBIT sign_in_week_12 5 1
## 获取指定 key 指定区间的值为 1 的数量
## bitmap 也是采用 string 类型,一个字节可以存储 8 位,这里的 start,end 是指字节的
127.0.0.1:6379> BITCOUNT sign_in_week_12 0 0
(integer) 4
## 获取 bitmap 里面 bit 值为指定值的第一个位置
127.0.0.1:6379> SETBIT bit2 0 1
(integer) 0
127.0.0.1:6379> BITPOS bit2 0
(integer) 1
127.0.0.1:6379> BITPOS bit2 1
(integer) 0
## 对 bitma p进行操作,支持 AND、OR、NOT、XOR
127.0.0.1:6379> SETBIT bitop1 0 1
(integer) 0
127.0.0.1:6379> SETBIT bitop1 1 1
(integer) 0
127.0.0.1:6379> SETBIT bitop2 0 0
(integer) 0
127.0.0.1:6379> SETBIT bitop2 1 1
(integer) 0
127.0.0.1:6379> BITOP OR bitor bitop1 bitop2
(integer) 1
127.0.0.1:6379> BITOP AND bitand bitop1 bitop2
(integer) 1
127.0.0.1:6379> GET bitor
"\xc0"
127.0.0.1:6379> GET bitand
"@"
HyperLogLog
使用场景
HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,但存在 0.81% 的误差。
基数统计是在伯努利实验基础上,指的是在同样的条件下重复地、相互独立地进行的一种随机试验,其特点是该随机试验只有两种可能结果:发生或者不发生。
以最容易理解的抛硬币举例,每次试验抛到正面的硬币就停止,否则就继续。那么可以知道:抛 1 次停止概率是 1/2;抛 2 次停止的概率是 1/22;抛 k 次停止的概率是1/2k。对每次试验,抛掷 k 次停止的概率都是 1/2k。
假设做了 n 次试验,其中最大的抛掷次数是 4,那我们可以计算出做的试验次数 n 吗?结合上面的基础,发现在 n 和最大抛掷次数(k_max)之间存在估算关联:n = 2k_max,那么我们也就知道应该是做了 16 次试验。
因为大数定理对于数据量小的时候,会有很大的误差。而为了解决这个问题,HyperLogLog 引入了分桶算法和调和平均数来使这个算法更接近真实情况。
基于 HyperLogLog 的特性,非常适合做不需要很精确的网站 UV 统计。
ruby
## 向 HyperLogLog 中添加元素
127.0.0.1:6379> PFADD hyperloglog 1 2 3 4 5 5
(integer) 1
127.0.0.1:6379> PFCOUNT hyperloglog
(integer) 5
127.0.0.1:6379> PFADD hyperloglog 6 7 8 9
(integer) 1
127.0.0.1:6379> PFCOUNT hyperloglog
(integer) 9
## 支持两个 HyperLogLog 的合并操作
127.0.0.1:6379> PFADD hyperloglog2 8 9 10 11 12
(integer) 1
127.0.0.1:6379> PFMERGE hyper_merge hyperloglog hyperloglog2
OK
127.0.0.1:6379> PFCOUNT hyper_merge
(integer) 12
发布订阅
使用场景
Redis 发布订阅是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。与现在经常使用的 MQ 机制很像,如果想实现 MQ 的发布订阅功能,又不想引入过多的中间件,可以尝试使用 Redis 的发布订阅功能。不过由于不具备传统 MQ 的高可用、消息堆积、重试等功能,所以并不能完全替代传统 MQ。
使用示例
监听某个 channel。
当监听的 channl 发布了消息后,能自动感知。
事务
使用场景
Redis 的事务,并不是预先执行的,而是先放入队列中缓存,当 EXEC 的时候才执行。当其中语句有语法错误(不需执行就知道错误)的时候是可以回滚的,否则并不会回滚,所以不能完全当成数据库事务一样使用。
Redis 事务能保证的是在事务执行过程,其他客户端提交的命令不会插入到事务执行命令中间。
使用示例
ruby
## 开启事务,中间有非法语句,事务将失败
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET trans1 abc
QUEUED
127.0.0.1:6379> SET1 trans2 cde
(error) ERR unknown command `set1`, with args beginning with: `trans2`, `cde`,
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
## 开启事务,中间有错误类型使用,正常的语句将提交,不会回滚
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET trans3 abc
QUEUED
127.0.0.1:6379> SADD trans3 def
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> get trans3
"abc"
Pipeline
使用场景
Redis 的 Pipeline(管道)功能在命令行中没有,但 Redis 是支持 Pipeline 的,而且在各个语言版的 Client 中都有相应的实现。 Pipeline 与 SQL 中的批处理过程类似,所有命令会先在 Client 端"打包",统一发送至 Redis,Redis 将部分请求放到队列中(使用内存),执行完毕后一次性发送结果。当 Client 与 Redis 之间的网络延时比较大,Pipeline 将非常有用。整个的处理过程改变如下:
不过,Pipeline 期间将独占链接,期间内将不能进行非"管道"类型的其他操作,直到 Pipeline 关闭;如果你的 Pipeline 的指令集很庞大,为了不干扰链接中的其他操作,可以为 Pipeline 操作新建 Client 链接。
高可用
Redis 作为 AP 系统,高可用的考虑是必不可少的。Redis 主要使用了主从复制加以 Sentinel 监控、自动切换来保证集群的可用性,同时使用 RDB、AOF 等机制保证数据持久化能力。现在,Redis 更多的使用 Redis Cluster 模式,即保证了集群的高可用,又可以突破单机存储瓶颈,理论上可以做到无限扩展。
Sentinel
Redis 可以用 Sentinel 系统管理多个 Redis 服务示例,Sentinel 主要执行以下三个任务:
- 监控(Monitoring):Sentinel 会不断地检查主服务器和从服务器是否运作正常
- 提醒(Notification):当被监控的某个 Redis 服务器出现问题时,Sentinel 可以通过 API 向管理员或者其他应用程序发送通知
- 自动故障迁移(Automatic failover):当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。
一个 Sentinel 可以与其他多个 Sentinel 进行连接,Sentinel 之间可以互相检查对方的可用性,并进行信息交换。Sentinel 可以通过发布与订阅功能来自动发现正在监视相同主服务器的其他 Sentinel,Sentinel 也可以通过询问主服务器来获得所有从服务器的信息。
一次故障转移操作由以下步骤组成:
- 发现主服务器已经进入客观下线状态
- 对我们的当前任期进行自增(Raft Leader Election 过程),并尝试在这个任期中当选
- 如果当选失败,那么在设定的故障迁移超时时间的两倍之后,重新尝试当选。如果当选成功,那么执行以下步骤
- 选出一个从服务器,并将它升级为主服务器
- 向被选中的从服务器发送 SLAVEOF NO ONE 命令,让它转变为主服务器
- 通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel,其他 Sentinel 对它们自己的配置进行更新
- 向已下线主服务器的从服务器发送 SLAVEOF 命令,让它们去复制新的主服务器
- 当所有从服务器都已经开始复制新的主服务器时,领头 Sentinel 终止这次故障迁移操作
持久化
Redis 的持久化主要支持 RDB 跟 AOF 两种方式:
- RDB持久化:在指定的时间间隔能对 Redis 的数据进行快照存储
- AOF持久化:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 Redis 协议追加保存每次写的操作到文件末尾。Redis 还能对 AOF 文件进行后台重写,使得AOF 文件的体积不至于过大
RDB
RDB 是一个非常紧凑的文件,它保存了某个时间点 Redis 服务的数据集,非常适用于数据集的备份,与 AOF 相比,在恢复大的数据集的时候,RDB 方式会更快一些。
在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 Redis 的性能。
RDB 虽然可以配置不同的保存时间点(默认是 5 分钟),但如果配置的时间较短,那么这个任务将是一个比较繁重的工作。因此在权衡性能后,使用 RDB 方式可能会丢失几分钟的数据。
RDB 需要经常 fork 子进程来保存数据集到硬盘上,当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求。
AOF
AOF 为 Redis 提供了更实时的持久方案,通过使用不同的 fsync 策略:①无 fsync(交由操作系统决定);②每秒 fsync;③每次写的时候 fsync。在默认使用每秒 fsync 情况下,Redis 依然能保持很好的性能,一旦出现故障也最多丢失 1 秒的数据。
Redis 在 AOF 文件体积变得过大时,会自动在后台对 AOF 进行 rewrite 操作(合并重复命令),rewrite 后的新 AOF 文件仅包含恢复当前数据集所需的最小命令集合。AOF 文件有序地保存了对数据库执行的所有写入操作,这些写入操作以 Redis 协议的格式保存,因此 AOF 文件的内容非常容易被读懂。
但对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。在一般情况下,每秒 fsync 的性能依然非常高,而关闭 fsync 可以让 AOF 的速度和 RDB 一样快。
混合模式
在 Redis 4.0 之后支持了 RDB + AOF 的混合持久化方案,混合持久化结合了 RDB 持久化和 AOF 持久化的优点:RDB 文件小易于故障快速恢复,AOF 能够保障数据丢失最小化。
混合持久化是通过 rewrite 完成的,与普通 AOF 不同的是:当开启混合持久化时,fork 出的子进程先将共享内存副本全量的以 RDB 方式写入 AOF 文件,然后再将 rewrite 缓冲区的增量命令以 AOF 方式写入到文件,并将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。简单的说:混合模式下,AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。
Redis 常见问题
过期策略
Redis 淘汰过期的 key 的方式只要有两种,主动和被动。
- 主动方式:当客户端尝试访问某个 key 时,如果 key 已经过期将主动进行 key 的过期删除,这种方式也叫惰性淘汰
- 被动方式:仅依靠访问时删除是远远不够的,因为会有大量 key 访问频率不是那么高,而导致无法删除。Redis 将定时检测 key 是否过期而进行删除,Redis 每秒将进行 10 次的过期检测,具体步骤是: a. 随机抽取 20 个 keys 进行相关过期检测 b. 删除所有已经过期的 keys c. 如果有多于 25% 的 keys 过期,重复步奏 a
淘汰策略
当 Redis 的数据集达到了 maxmemory 的限制时候,需要一些策略来拒绝或者清理 Redis 来保证系统可以正常运行下去。
可以使用 maxmemory-policy 配置项来指定相应的策略:
- noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
- allkeys-lru:尝试回收最少使用的键(LRU),使得新添加的数据有空间存放
- volatile-lru:尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放
- allkeys-random:回收随机的键使得新添加的数据有空间存放
- volatile-random:回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键
- volatile-ttl:回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放
Redis 的 LRU 算法并非完整的实现,这意味着 Redis 无法选择最佳候选 keys 进行回收。Redis 尝试运行一个近似LRU的算法,通过对少量 keys 进行取样,然后回收其中一个最好的 key(被访问时间较早的)。
不使用真实的 LRU 实现是因为需要太多的内存,而近似的 LRU 算法对于应用而言也完全够用。
常见问题
缓存雪崩
缓存雪崩是指缓存在同一时间失效,导致大量的 Cache Miss,可以通过随机过期时间来避免,也可以通过延期 key 过期时间来避免。
缓存击穿
缓存击穿主要是说热点 key 失效,当热点 key 失效时候,大量请求将达到后端,并尝试更新缓存。解决办法是热点 key 不失效,或者更新缓存时候需要加锁。
缓存穿透
缓存穿透是支持访问数据库中不存在的 key,导致数据无法被缓存,全量请求到后端。解决办法是:首先需要增加查询校验,其次可以将 null 或指定值进行缓存,或者使用 BloomFilter 进行异常 key 过滤。
双写一致性
在单机情况下,经常会因为多线程对同一个数据处理,导致数据不一致性。在分布式情况下,这种情况将更容易发生。如上图所示,当发生这种处理过程的时候,将导致最新的数据更新丢失,因此提出了"延时双删"的概念。
延时双删,顾名思义即在第一次删除后,根据业务情况延迟一定时间再进行一次删除,以求最大程度的保证数据一致性。因为如果延迟的时间较大,那么延时期间内请求到的数据可能是失效的,而如果延迟时间较小,又可能导致双删失效。
Bigkey
Bigkey 即数据量大的 key ,由于其数据大小远大于其他 key,导致经过分片之后,某个具体存储这个 Bigkey 的实例内存使用量远大于其他实例造成内存分配不均。在请求的时候,由于数据量过大很可能会造成阻塞,甚至被判定为下线进而进行主从切换。如果这个 key 同时是热点 key,非常容易造成网络阻塞。
发现 Bigkey 可以通过 Redis 自带的 --bigkeys 命令,或者使用 scan 命令对 key 逐个进行分析。在发现 Bigkey 之后,主要原则就是删除、拆分、裁剪。
热点 key
热点 key 主要是对高热访问的一些 key 做处理时候需要的一些注意事项。
首先在构建过程中要保证同时只有一个线程会去访问数据库获取值,过期策略可以根据情况适当加长甚至永久有效。
在使用的时候需要提前进行预热,同时利用 Java 本地缓存缓解压力,必要时候需要有服务降级预案。