Redis 实用小技巧——记一次 Redis 「大扫除」行动

永不过期的键

最近一直在做服务器成本优化的工作。在整理 Redis 使用情况的时候,发现存在的问题还真不少。其中一个比较头疼的问题,就是某个实例未设置过期时间的键,居然有八十万之多——这是多么恐怖的数字!

所有键加起来才一百万左右,未设置过期时间的键占比超过了 80%。

正常来讲,我们业务使用的 Redis 不会有这么多键,而且大部分都未设置过期时间,这里面肯定也是有问题的。

所以,第一步,就是需要把「问题」键找出来。

怎么找呢?

在不了解所有键分布的情况下,我一般先从RANDOMKEY这个命令入手。这个命令可以随机返回库里的一个键。

如果某种类型的键分布比较集中的话,通过RANDOMKEY命令,会比较大几率命中这种类型的键。

经过一番操作以后,我发现比较频繁出现的是这种键:

128e29fa66cabb33b5938c2de3cf0656

当看到这种键的时候,我第一反应就是有人用md5值作为键。通过对比,发现这种键具备md5值的特点:

  • 32 位固定长度。
  • 由数字 0-9 和小写字母 a-f 组成。

虽然不清楚这种键具体有多少,但通过简单的概率理论推测,这种键占比大概在 60% 左右。

在和业务同事沟通过以后,确认这种键只是做缓存使用,缓存时间正常应该设置为一天,因为大意导致没有设置过期时间。键生成规则正是几个参数经过md5运算得到的值,和我猜想的一样。

这里不得不又提到一个老生常谈的问题——键名设计规范的问题。

这种通过md5值直接命名键名的方式,存在以下明显的缺点:

  • 不具备可读性。看到这种键,完全不知道干嘛用的。
  • 存在md5撞库的可能性。md5只是生成一个固定的哈希值,哈希值本身具有随机性,但不具备唯一性。
  • 维护性差。如果不了解具体业务的,碰上这种键,根本不敢动。

既然已经找到了「罪魁祸首」,下一步问题就是如何把这些键全部找出来,然后设置一个过期时间了。

把问题键揪出来

现在,我们要做的,就是如何在百万个键中,把以md5值这种命名规则的键给找出来。

思路一

假设我们的键符合以下规则:

USERINFO:[1-10000之间的整数]

如果想找出符合条件的所有键来,可以通过遍历整数部分的后缀,然后拼接成一个完整的键名,再通过EXISTS命令查看是否存在。

理论上讲,通过KEYS USERINFO:*命令,也可以将所有的键找出来。

温馨提醒: 这里强调了,只是理论上,实际千万别这么干。因为线上KEYS命令就是个潘多拉魔盒。哪怕某次KEYS命令很快返回了你想要的结果,也不要心存侥幸,因为说不定哪次就把你抬坑里去了。

这种具有明显规律性的键名,都可以采用限定区间,然后遍历的方式来获取目标键。

但是这种方式对于md5值生成的键还适用么?

md5长度为 32 位,每一位都有 0-9 和 a-f 共 16 种可能。所有md5值的范围为0000000000000000000000000000000fffffffffffffffffffffffffffffff,共 2 ^ 128 个值。

呃,PASS……

思路二

既然通过事先生成的样本到目标库判断的思路行不通,那只能反其道而行之了。

也就是从目标库取出样本,然后判断是否符合md5值命名的规则。

但是这种方式实现起来也有难度,数据库里键的数量过大,这里就涉及到一个从海量的 Redis 键中,取出符合条件的键的问题了。

KEYS是个危险命令,会阻塞线程,这里自然就不考虑了。

除了KEYS命令,我们还可以使用SCAN命令进行遍历。SCAN命令使用方法如下:

SCAN cursor [MATCH pattern] [COUNT count]

我们可以借助MATCH参数进行模糊匹配。不过SCAN命令的匹配模式,仅支持glob风格的正则模式,即通配符相关的简单正则模式。所以这里如果想匹配md5格式的话,就有些难度了。

另外,即使支持,我们也建议尽量不要直接在命令中使用复杂的匹配模式,这会增加 Redis 命令的处理时间。

我们可以借助脚本语言,使用SCAN命令进行遍历,在程序中进行正则判断,获取到目标键。

这种方式实现起来没有什么难度,不过需要注意控制每次返回元素的数量,数量不能太多,以免引起阻塞。

除了需要遍历全部的数据外,SCAN也算是比较稳妥的处理方式了,毕竟不会阻塞线程,只是调用命令的次数会多一些。

还有其他处理方式吗?因为实在不想写代码。

有,思路三。

思路三

这也是我经常使用的方式了。之前在 《Redis 实用小技巧——一文教你如何选择合适的 Key 类型》 一文中做过介绍,就是借rdb备份文件和rdbtools工具进行操作。基础用法大家可以参考下之前的文章,这里就不做介绍了。这里仅说一下实现的步骤。

其实这种实现思路也比较简单,主要处理好以下几个关键步骤就可以了。

1. 导出所有未设置过期时间的键

使用rdb命令从rdb文件导出所有未设置过期时间的键。

rdb -c justkeyvals -x -f target.csv ./origin.rdb
  • -c justkeyvals:导出键和值的格式
  • -x:通过协议命令排除过期的键
  • -f target.csv:结果输出到目标文件
  • /origin.rdb:原始的rdb文件

通过这一步,将所有的未设置过期时间的键导入到目标文件中。

2. awk 命令匹配目标键

这里之所以把值也导了出来,是为了通过值的格式进一步限制范围,避免其他业务也有使用了md5值的命名规则,也被一并处理。

接下来,我们需要把符合条件的键筛选出来。使用awk命令实现:

cat target.csv | awk -F ' ' '{if($1 ~ /^[0-9a-fA-F]{32}$/ && $2 ~ /keyword/){print $1}}'
  • -F ' ':指定空格为awk分隔符
  • $1 ~ /^[0-9a-fA-F]{32}$/:键名符合md5值格式
  • $2 ~ /keyword/:键值包含keyword关键字

通过这一步,可以将所有键名为md5值格式的键筛选出来。

3. awk 命令生成处理过期命令

通过wc命令统计,发现筛选出来的键有六十万左右。

这么多的键,肯定不能一个个设置过期。通过程序脚本设置过期倒是可行,但是已经到这一步了,还是不想写代码怎么办?

那就直接生成一个 Redis 命令的文件,通过redis-cli客户端直接以外部文件的形式批量运行命令。

我们可以先生成一个 Redis 命令文件。

生成方式当然还是使用强大的awk命令了。直接上命令:

cat target.csv | awk -F ' ' '{expireAt=86400*6+int(86400 * rand());if($1 ~ /^[0-9a-fA-F]{32}$/ && $2 ~ /keyword/){print "EXPIRE " $1 " " expireAt}}' > redis-script.txt

这里就是在输出的时候,拼接了一个EXPIRE字符串和一个expireAt变量。expireAt定义在命令的前边,表示过期时间。

这里过期时间定义为 6 天的秒数加上 0 - 86400 之间的一个随机数,是为了避免六十多万的键集中过期,造成「缓存雪崩」的情况。

4. 预估脚本执行时间

在正式执行脚本之前,保险起见,我们还需要做一步评估,预估一下整体的执行时间。

我们可以先确定执行一条命令所用的时间,然后再乘以基数,进而得到整体的耗时。

这里,我们使用事务的方法,在命令执行前后各执行一次TIME命令,进而得到执行一次命令的时间。

128.0.0.1:6379> MULTI
OK
128.0.0.1:6379> TIME
QUEUED
128.0.0.1:6379> EXPIRE 06c97b4b15ac2b94068bc2576aeb9ad5 568990
QUEUED
128.0.0.1:6379> TIME
QUEUED
128.0.0.1:6379> EXEC

执行结果如下:

1) 1) "1718777686"
   2) "424625"
2) (integer) 1
3) 1) "1718777686"
   2) "424639"

经过计算得出,执行一次命令的时间在 14 微秒左右。则六十万数据执行完一遍命令,所需的时间大概为:

600000 * 14 = 8400000 微秒 = 8.4

因为是批量执行的单条命令,所有时间基本在可控范围内。

5. 外部执行 Redis 命令

接下来,就是通过redis-cli的方式,从外部脚本文件中批量执行命令了。执行方式如下:

cat redis-script.txt | redis-cli -h 127.0.0.1 -a *** --pipe
  • --pipe:启用 pipe 协议,它不仅仅能减少返回结果的输出,还能更快地执行指令。

[ 开始运行—— ]

[ DONE! ]

实际上,只用了五秒不到的时间,命令就执行完了,对业务并没有造成什么影响。

6. 验证结果

对设置完过期的键进行抽样检测。

我们分别从头部,中部和尾部取部分样本,使用TTL命令查看过期时间,发现都已经设置了过期时间,说明脚本执行的没有问题。

总结

如果使用了云 Redis 的话,基本都会进行定期备份。有了备份的rdb文件,我们就可以做很多事情了。

像 key 活动监控、慢日志、bigkey 这些基础功能,现在很多云平台都已经支持。

不过,像本文介绍的这种针对具体键进行的操作,rdb文件可以发挥的空间还是很大的。

有时候,偷懒,反而是一种捷径。

本作品采用《CC 协议》,转载必须注明作者和本文链接
你应该了解真相,真相会让你自由。
本帖由 MArtian 于 8个月前 加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 6

好好好,不过怎么样都可能处理到其他业务的md5值吧

8个月前 评论
快乐的皮拉夫 (楼主) 8个月前
fofome 8个月前

:+1:

7个月前 评论

皮拉夫的文章我是篇篇都收藏 :+1:

7个月前 评论
快乐的皮拉夫 (楼主) 7个月前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
文章
41
粉丝
121
喜欢
711
收藏
764
排名:249
访问:3.8 万
私信
所有博文
社区赞助商