什么是 BigKey 与 MoreKey
在 Redis 中 BigKey 主要是指单个 Key 所对应的 Value 体积过大。与之相对的 MoreKey,则表示 Redis 中存在的 Key 数量过多。
由于 Redis 工作线程是单线程执行,对于 BigKey 与 MoreKey 若操作不当,可能会导致主线程长时间阻塞,从而影响正常业务。
慢查询优化
首先,我们使用 --pipe
方式向 Redis 中写入 100 万条数据,模拟 MoreKey 慢查询场景。
for((i=1; i<=100*10000; i++)); do echo "set k$i v$i"; done | redis-cli --pipe
若在这时候使用 keys
命令,由于其不存在 offset 或 limit 之类的参数,Redis 会将所有满足条件的数据都扫描一遍!在我的电脑中执行 keys *
命令耗时 8.85 秒。
999986) "k668369"
999987) "k460512"
999988) "k183483"
999989) "k888390"
999990) "k564755"
999991) "k56696"
999992) "k790600"
999993) "k756496"
999994) "k105867"
999995) "k981794"
999996) "k809782"
999997) "k976267"
999998) "k609291"
999999) "k378110"
1000000) "k29233"
(8.85s)
127.0.0.1:6379>
在这 9 秒左右的时间内,其他正常业务都将要阻塞等待,在生产环境中可能会出现查询超时而后将请求全部打到数据库上,导致缓存雪崩、数据库宕机等事故。
SCAN 命令
针对以上的问题,可以使用 scan
命令进行查询,这是一个基于游标的迭代器。
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
第一次调用可以使用 0 作为游标,每次调用都会返回一个更新后的游标用于下一次调用,直到再次返回 0 时表示完成了一轮迭代。
127.0.0.1:6379> scan 0
1) "196608"
2) 1) "k917183"
2) "k95839"
3) "k836737"
4) "k194992"
5) "k742723"
6) "k497883"
7) "k683756"
8) "k571367"
9) "k963538"
10) "k932885"
11) "k541435"
127.0.0.1:6379> scan 196608
1) "163840"
2) 1) "k577059"
2) "k812468"
3) "k612519"
4) "k894783"
5) "k798154"
6) "k231542"
7) "k184429"
8) "k743278"
9) "k604698"
10) "k198804"
11) "k27792"
12) "k492665"
参考阿里 Redis 开发手册慢查询优化:
- 尽量不要使用复杂度
O(N)
及以上的命令,对于数据的聚合操作,放在客户端做 - 执行
O(N)
命令,保证N
尽量小(推荐 N<=300),每次获取尽量少的数据,让 Redis 可以及时处理返回
禁用高危命令
针对 Keys *
这种高危操作,最好能在生存环境禁用避免误操作。在 redis 配置文件中,可以对命令重命名,空字符串则代表禁用命令。
rename-command keys ""
rename-command flushdb ""
rename-command flushall ""
BigKey 排查
BigKey 的产生本质上是由于将过多的数据存放在同一个 Key 中,导致其 Value 的体积过大。
参考阿里 Redis 开发手册:string
控制在 10kb 以内,hash
、list
、set
、zset
元素个数不超过 5000 个,反之则称为 BigKey。
Redis 客户端提供了 --bigkeys
参数用于排查 BigKey,该程序内部是基于 SCAN 命令实现,因此可以在生产环境中使用。
redis-cli --bigkeys
使用该命令扫描 BigKey 时,会导致 Redis 的 OPS 突增,为了减低扫描过程对 Redis 的影响,最好添加 -i
参数设置每次 SCAN 间隔时间。
扫描结果对于容器类型的 Key,只能扫描出元素最多的 Key,并不代表其占用的内存也大,还需要根据实际情况作进一步评估。
可以使用 MEMORY USAGE
命令获取指定 Key 实际占用的内存字节大小。
MEMORY USAGE key [SAMPLES count]
优雅删除 BigKey
对于任何一个 Key 都能直接使用 del
命令删除,但是 del
命令会阻塞主线程,当删除 BigKey 时长时间阻塞会影响其他业务进行。
渐进式删除
如果 Redis 版本在 4.0 以下,或需要对复杂数据类型进行更精细的控制时,可以考虑使用渐进式方式删除 BigKey。其本质思想是使用 scan
命令遍历 BigKey,分批次逐渐删除元素,避免主线程长时间阻塞。
- String:可以直接使用
del
删除,若过于庞大则使用unlink
命令异步删除 - List: 使用
ltrim
命令多次裁切列表,直到足够小时再使用del
删除 - Set: 使用
sscan
命令每次获取部分元素,再使用srem
命令将其删除 - Sort Set: 与 Set 类似,使用
zscan
+zremrangerbyrank
- Hash:同上,使用
hscan
+hdel
Lazy Free
在 Redis 配置文件中,针对 4 种应用场景分别提供了对应的配置项,默认全是关闭的。
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
- lazyfree-lazy-eviction: 当 Redis 内存使用达到 Max Memory 并设置有淘汰机制时,是否采用 LazyFree 机制
- 该场景启用 LazyFree 可能内存释放不及时,会导致内存超用(超过 Max Memory限制)
- 该场景需结合实际业务测试,生存环境不建议开启
- lazyfree-lazy-expire: 当设有 TTL 的 Key 过期后,被 Redis 清理是否采用 LazyFree 机制(建议开启)
- lazyfree-lazy-server-del: 有些命令在处理时,会隐式的携带
del
操作,针对这种情况是否采用 LazyFree 机制(建议开启) - replica-lazy-flush: 针对 Slave 进行全量数据同步时,会使用
flushall
来清理自己的数据,是否采用异步 flush 机制(建议开启)- 可减少全量数据同步耗时,从而减少主库因输出缓冲区暴涨引起的内存使用增长