开场白:本文不谈Redis的Gossip,也不然Raft,更不谈,只是本着求真务实的态度,聊聊在实际业务中遇到的Redis相关的问题。
本文汇总的都是在实际工作中出现过的常见问题以及自己曾经记录的注意事项。
1、热点key可能是一个随时会被引爆的雷
之前单位有一个实际案例是某个系统的交易成功率低于最低告警值 ,其产生原因就是Redis存在两个热点key,且这两个key都在同一个master节点上(RedisCluster集群),导致在高峰期间对应节点访问流量过大,造成了严重的访问倾斜。
对于这种问题,我们可以通过两种方式解决。
1、增加一层本地缓存,减少远程分布式缓存Redis的访问,基本上多级缓存都这么干;
2、考虑到一致性的问题,用不了本地缓存,像秒杀场景的活动库存这种,就可以将热点key分散存储,一个key拆成多个子key存储。比如某电商进行茅台抢购活动,首先将茅台商品活动库存放到Redis中,茅台就是一个热点key,如maotai,1000,此时就可以将热点key分散存储,茅台抢购是需要预约的,可以将所有预约用户进行分组,Redis会根据组存储活动库存,key可以是 maotai:groupId1 300,maotai:groupId2 300,maotai:groupId3 400,通过这种方式把整个流量分散到不同的节点上,减轻单节点压力。
当然,有人会问,那出现有些节点抢购完了,有些没抢购完怎么办?对于这个问题,首先是我们用户都是平均hash到不同的节点上的,请求总体上是平均的;其次就算是出现了这种情况,对于抢购这种营销活动,我们是允许少卖的,超卖却是不可以,同样因为性能降低导致系统崩溃也是不允许的。总结说就是我们要做到两权相害取其轻。
2、避免bigKey
另一个实际案例同样是某系统的交易超时和成功率低预警,以及机房传输设备有端口心跳告警,其产生原因是存在bigKey,且进行批量操作(hgetall,hsetall),在交易并发量增大时,批量查询存储操作对Redis性能影响较大,查询缓慢进而导致交易超时。
尽管Redis在Redis4.0引入了后台多线程,Redis6又引入了IO多线程,但Redis的主要命令执行依然是单线程的。如果对单个key进行批量操作,如hashKey的 hgetall,hsetall,执行的时间复杂度是O(N),当并发上来的时候,这种操作是非常低效的,很容易造成客户端阻塞。此外也可能会引发网络阻塞。因为每次获取大 key 产生的网络流量较大,举一个极端的例子,一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说比较难以承受。
我在之前写的文章已经详细说了这个问题以及解决办法,传送门:
Redis的big key问题介绍以及监控手段
3、在使用lettuce时,记得配置网络拓扑刷新
在Java领域,Redis客户端还是比较多的,如jedis,redisson,lettuce等,Springboot2.x以后默认是lettuce,由于其高性能以及并发安全性使其得到了很广泛的使用,但其有一个最大的弊端是默认不进行网络拓扑刷新。那啥是网络拓扑刷新呢?即RedisCluster集群,如果某个master节点异常宕机,集群会自动进行重新选主,某个slave会被选举成为master,然而lettuce客户端默认并不会感知,导致请求还会达到不可达的节点上,从而出现超时,严重的会因为连接池的限制影响到其他正常的master请求。
springboot2.3以后lettuce可以进行网络拓扑刷新配置:
spring:
redis:
lettuce:
cluster:
refresh:
adaptive: true
period: 10000 # 10秒自动刷新一次
如果不想用lettuce,可以切换成jedis或者Redisson都可以,这两者都默认实现了网络拓扑刷新。
这个问题在我前东家和现在的公司都出现过,前公司的解决方案是直接把客户端换成jedis,现在的解决方式就是配置网络拓扑刷新。
4、使用Redis客户端,根据实际情况配置连接池
这个和数据库连接池是一致的,主要是配置最小空闲连接和最大空闲连接,min-idle和max-idel,这两个配置不是一拍脑门就决定的,而是要根据业务的实际情况不断去调试,这和所有的池化原理都一样,如连接池,线程池都要根据业务调优。
连接池的问题在jedis上更为突出,因为lettuce采用了Netty,使用了多线程+IO多路复用(基于NIO)实现的异步非阻塞的事件驱动机制,性能上要比jedis好太多,jedis非常容易因为某些请求长时间占用线程导致连接池耗尽。因此目前生产上出现Redis连接池问题的基本上都是jedis客户端引起的,我碰见过的几起事故都是jedis配置问题导致的。当然,这并不是绝对啊,lettuce同样有出现问题的可能,只是概率小罢了。
5、尽量避免大量使用批量操作
批量操作可能大家用的比较多,因为可以减少请求次数,但用多了同样是会出现问题的,如果频繁的进行批量操作,由于Redis的命令执行是单线程的,其他命令就会阻塞住,如果批量操作比较耗时的话,就将是灾难性的。
6、生产环境禁用的命令
- keys * :排在首位, 重中之重,用了它,Redis就完蛋了,很有可能导致整个集群出现大面积故障
- flushall: 同步操作
- flushdb: 同步操作,丢失当前database的所有数据,出现堵死现象
- save: 同步操作
- bgsave: 尽管是异步操作,但需要同步fork子线程,FORK引起COW内存消耗,有导致大面积OOM的风险。这个主要还是Redis自己执行就OK了,不需要我们操作
- shutdown: 导致Redis关闭、数据丢失和故障转移
- del,删除命令可以使用unlink.
7、Key的设计规范
- Redis缓存场景,建议Key都设置TTL值,保证不使用的Key能被及时清理或淘汰,使内存复用。
- 建议Key使用":"字符进行分层,如 hbnnmall:coupoun:uid。
- Key名字本身是String对象,最大长度512MB,也是big key的范畴。
- Key的长度小于30个字符,Key内容本身分占用1到多份内存容量。
- 按业务功能命名key前缀,防止key冲突覆盖,同时方便运维管理。
8、程序架构规范
- 查看命令时间复杂度,官方文档Commands,每个都对应有Time complexity属性;如HGETALL命令Time complexity: O(N) where N is the size of the hash.时间复杂度为O(N)的常见命令: keys mget mset hgetall。
- redis的命令是单线程执行的,如果命令耗时过长,命令独占server,其间不能响应其他命令,导致服务超时。如果执行时间比较长,甚于导致判断节点为下线状态,触发集群故障转移。
- 避免使用时间复杂度为O(N)的访问模式或命令;对元素比较多的集群key使用时间复杂度为O(N)命令。
- Cluster模式中,热点Key和大容量Key尽量设计"打散”;避免集群不均,导致某个分片QPS“过载“和容量过大。