大家好,我是程序员鱼皮。
很多小伙伴都知道缓存的好处,从数据库加载数据过慢时,直接上 Redis 缓存!
的确,Redis 高性能 KV 存储是后端开发提升性能的一大利器,但是有没有想过,如果使用姿势不对,使用 Redis 后,性能反而会更慢呢?
今天就来盘下使用 Redis 性能变慢的几个原因以及一些应对手段。
1、网络和通信导致的延迟
比如我们现在要往 Redis 里面写入多个 key 和值:
key | value |
---|---|
name | yupi |
gender | male |
base | shanghai |
... | ... |
很多同学会采取一条一条塞入的方式来完成这些键值对的写入,如图:
可以看到,下一条数据的写入需要等待上一条的返回,这个等待时间除了命令的处理时间, 其实网络通信的时间也占据了很大一部分 。
就好比我们网购了 5 件衣服,都送到了快递驿站,此时我们是去驿站一件一件拿回家快呢?还是 5 件衣服一起拿快呢?
答案显而易见,肯定是一起拿快,如果一件一件的拿,很多时间都消耗在路上了!
同理,对于上面 Redis 这种场景,我们需要使用 MSET 这样的聚合命令,通过 批量操作 来提升性能。如图,一趟搞定!
我本地写了段脚本来实际测试了一下,对比使用 for 循环插入 2W 条记录,和利用 mset 命令一次性插入 2W 条数据的耗时。
结果,for 循环花了 5472 毫秒,mset 花了126 毫秒,它们之间差了 40 多倍 !
由此可见,这种聚合命令在某些时候下,提升性能的效果还是十分可观的!
类似的聚合命令还有很多,比如 MGET、MHSET、HMGET 等等。
除此之外,也可以使用 Pipeline 一次性打包多条命令执行,更进阶的还有 lua 脚本,这里就不多展开了。
2、忽略复杂度高的命令
很多同学都默认 Redis 很快,于是用起 Redis 没啥负担,就是几行代码的事情嘛~
其实像一些普通命令,比如 SET 或 LPUSH 这种问题确实不大。
用我的一台小破机器测试,一条 set 命令消耗的时间在 10 毫秒以内。
但是有一些命令却不是,比如 SORT、LREM、SUNION。举个例子,比如有两个大集合,存了很多很多数据,此时你要取它们的交集,想想是不是很耗时?
我在 200W 条数据量的情况下使用 SUNION 命令测试,耗时近 5000 毫秒,跟正常的一条 set 的10 毫秒可是差了 500 倍!
而且需要注意,Redis 执行命令是单线程的!如果你前面执行了一个比较耗时的命令,假设此时并发度很高,那么就会有一堆命令排队等着前面耗时的那条命令,这个时候就会产生阻塞。
想想看,本来 redis 能处理 500 条命令,现在只能处理一条了,这种情况频繁一点,在高峰期对业务的影响就会很大。因此在生产环境中,需要慎重的使用这些命令,仔细评估集合的数据量,如果数据量不大,那么才能使用。
对了,这里需要特别强调一个命令:
keys
,很多生产环境的问题都是因为这条命令导致的。我对这个命令记忆尤其深刻,因为之前有个同事因为执行了这个命令导致线上服务雪崩了!
这个命令它会扫描 db 所有 key ,如果比较耗时,特别是当前 reids 有很多 key 的情况下,很容易造成服务的崩溃,从而引发雪崩!
做个狠点的测试,插入 1 亿条数据,然后执行下 keys 来看看到底得耗时多久!开始!
10分钟过去了....
20分钟过去了...
???中间没忍住想利用可视化工具打开看看已经插入了多少条,然后它崩了!!
行吧,1 亿数据确实有点多,我放弃,不插入了。
重启了 redis desktop manager 一看,已经插入了2900w条了
于是在 2900w 条数据时,执行 keys 命令,消耗了大概 50 多秒。
在执行 keys 命令的时候,哪怕执行一个普通的 get 命令,也要一直被阻塞。有的时候,单次查询慢,不一定是查询代码的问题。
所以,建议在生产上禁用这类命令,防止一些同学误用产生重大事故!
3、key的集中过期
除了 keys 的问题,之前在生产环境还遇到一个莫名其妙的 Redis 问题。
当时排查的时候,把我头都快搞秃了!
当时遇到的问题就是 key 的集中过期 。
Redis 淘汰键值对有两种方式:
惰性淘汰。当命令请求到这个 key 时,看下它过期没,如果过期则清理
主动淘汰。Redis 每 100 毫秒会随机扫描 20 个 key,删除其中过期的 key,如果过期率超过 25 % ,则会继续这个过程,过期率超过 25 % ,则会继续这个过程,过期率超过 25 % ,则会继续这个过程。没错,会一直重复这个步骤,直到过期率低于 25 % 或者累计耗时超过 25 毫秒才会终止这个步骤。
就是这个主动淘汰机制,使得如果有大批的数据在同一时间到期, 那么主要淘汰每次时长都要拉满,这其实就等于主动给 Redis 加压!
好家伙,这种时间的拉长在慢日志里面是查不到的,因为它不是因为命令本身的耗时长,所以当时排查了非常久,让我摸不住头脑的同时,也让我摸不到头发了。
所以在生产上要避免一大堆 key 同时到期,我们在设置过期时间的时候, 可以增加一些随机数来打散它们 。比如下列代码:
expire(key, time + random(600))
4、bigkey
最后还有一个非常重要的问题点,也是大伙在使用上需要着重关注的点,就是 bigkey 问题。
可以理解为目前的 key 所占的内存比较大,可能是它的值比较多,或者每个值占用的内存比较多。
就好比一个删除(DEL)操作,在我们眼里它不像两个集合交集这么复杂,但是在 bigkey 场景下,它可能就会出现问题!
我们都知道 Redis 占用内存资源,而内存是有限的,因此如果有不需要的内存需要及时释放,所以就会 DEL 某个 key 来释放内存,然后这个 key 又是个 bigkey ,因此释放的内存比较大,这样一来耗时就会比较久,所以一个简单的 DEL 命令都可能会在高峰期造成阻塞。
就好比咱们平日每天扔垃圾,早上出门把昨晚的垃圾一提一扔,轻松!
假设你假期在家蜗居了 7 天,点了 7 天的外卖,然后你这个懒鬼一点都不想出门,垃圾堆着就多了,此时长假结束,你要出门扔垃圾了,请问你能一趟扔完吗?
针对删除的命令,在 Redis 4.0 之后,可以通过 unlink 代替 del,unlink 释放内存是放在后台线程执行的,不会阻塞主线程,6.0 版本开启 lazy-free 后,释放内存都是放在后台线程执行。
不过以上仅仅只是删除的优化,在业务上我们还是需要避免 bigkey 的产生,对于一些已有的 bigkey,要及时做拆分。
以上就是本期分享,希望大家使用 Redis 的时候,多一个心眼,避免事故的发生~
👇🏻 点击下方阅读原文,获取鱼皮往期编程干货。
往期推荐