Redis是高性能的缓存服务组件,可以提供10万+级别的读写请求量。但是如果使用不规范或者存在大风险的操作,导致服务抖动耗时上涨,甚至出现服务不可用的情况。下面就对这些问题进行分析说明。
一、key名称
规范性:使用业务名作为前缀,用冒号分割。有子系统时,可以使用多个冒号或者下划线。比如:order:time:123456,data_123_456
间接性:在保洁规范性的同时,也需要注意key的长度,key过长会导致占用较多的内存空间。
避免转移字符:不能使用逗号,换行,空格,双引号,单引号,大括号等转义字符。
二、集合类操作
出现问题最多的超时问题,就是使用了O(N)的操作,导致服务超时,甚至服务不可用。
案例:
某服务需要获取每小时所有的在线用户情况,使用了一个hash结构存放在线用户的相关信息。正常情况下,改hash结构里只有1k以下的用户,每次进行hgetall这个key。但是某次突发紧急情况,用户数量突增至5万+。此时hgetall导致了大量的慢查询,并且拖累整个集群耗时急剧上涨。
分析:
hgetall命令的时间复杂度是O(N),其中N是集合元素总数,本例中当用户数量为5万时出现慢日志。可以通过缩小每次查询的集合数量,可以将一小时分成多段,分批次查询。比如把1小时范围的用户改为查5分钟范围的用户,分别查12次处理即可。
总结:
使用set,zset,list,hash等集合类的O(N)操作时要评估当前元素个数的规模以及将来的增长规模,对于短期就可能变为大集合的key,要预估O(N)操作的元素数量,避免全量操作,可以使用HSCAN,SSCAN,ZSCAN进行渐进操作。
集合元素数量过大在使用过程中会影响redis的实际性能,hash类元素个数建议尽量不要超过100,集合类、链表类数据尽量不要超过10k。元素数量过大可考虑拆分成多个key进行处理。
三、value大小
出现问题最多的就是用来大Key。大key的获取会导致超时,而且key过大会导致后端实例内存分布不均。集群扩容存在风险。
案例一:
某服务使用了一个list记录注册的用户ID。随着时间的增长,该list成为了一个又几百万元素的key。一个key的内存使用量突破到了4G。虽然每次都是只插入一个元素,但是日积月累,就成了一个大value的key。
案例二:
某服务使用redis存放用户的权限关系树,将该用户拥有的所有关系树的信息集合序列化成json字符串。使用的时候再反序列化成对象列表使用,value大小超过2MB。由于数据比较大会触发拆包,从而降低redis的吞吐量。
分析:
数量比较多的时候可以考虑改用hash,list等结构存储,每一个field存放一些信息,如果数量略微较大可使用hscan获取。如果数据量超过建议值,可以考虑拆分多个key。
当数据量较小的时候,建议使用string,当value偏大时也可以考虑怼她进行压缩以减少读取和写入对象时所需的网络带宽。对比压缩算法lz4、gzip和bzip2,看看哪个算法能够对被存储的数据提供最好的压缩效果和最好的性能。
string类型尽量控制在10KB以内。虽然redis对单个key可以缓存的对象长度能够支持的很大,但是实际使用场合一定要符合炒粉多大的缓存项,1k基本是redis性能的一个拐点。当缓存超过10k,100k,1m性能下降会特别明显。具体可以参考:大Key优化。
四、过期时间
案例:
某服务使用redis存放一个系统异常日志。当需要时,要迅速调用异常日志进行分析,正常情况下该服务所需要的是最近7天内的key。但是该服务没有对相关的key进行设置过期时间,导致集群内存使用量一直持续上涨。
分析:
要跟自己的业务场景,需要对key设置合理的过期时间。可以在写入key时,就要追加过期时间;也可以在需要写入另一个key时,删除上一个key。
总结:
不合理的过期时间或者不设置过期时间,就导致了大量的死key。对于测试的数据,请务必设置过期时间。
五、不必要的请求
案例一:
某集群在一次压测时发现,业务层请求量上涨了5倍的量。但是到了redis的监控上,请求量却上涨了10倍+。主要是业务在每次写入key前,总会进行一次ping操作,判断集群是否可用。
案例二:
某集群在使用redis存放了轨迹信息。但是轨迹只需要维持一段时间。该业务同学对每次写入的轨迹信息有设置时间,在需要删除旧的轨迹信息时,使用del对旧key进行删除,但是实际上旧key已经触发了过期删除。执行的del是空操作。
分析:
不必要的操作会占用一定的吞吐量,影响集群整体性能。redis的命令操作有一些返回值,比如ttl返回为-2时表示key不存在,就没有必要再进行del删除,lpush 时会返回key的长度等等。
总结:
有一些sdk,比如java和go的配置了Test相关的TestOnBorrow,TestOnReturn, TestWhileIdle等等。这些模块会发生多余的ping指令,可以将这些设置成flase,减少不必要的操作。
六、批量的操作命令
案例:
某系统需要往redis中导入一批司机信息,当前在线司机数量有5万个。采用循环hset 5万个,客户端操作5万次,5万*0.3ms(链路)+ 5万*0.01ms=15.5s。客户端超时。采用hmset5万个,客户端操作1次,0.3ms + 1s = 1s。服务端耗时飙升,redis实例慢查阻塞,引起大面积超时排队。优化:每次循环操作50个key。分1000次操作。
分析:
较大量的请求,可以使用hmset进行折中。减少redis操作次数同时提升处理速度,但是要考虑单次请求操作的数量,避免慢日志。另外对于hmset中,hash值建议不要超过100个,避免hgetall等操作阻塞实例。
总结:
在redis使用过程中,要正视网络往返时间,合理利用批量操作命令,减少通讯时延和redis访问频次。redis为了减少大量小数据网络通讯时间开销 RTT (Round Trip Time),支持多种批操作技术:
1、MSET/HMSET等都支持一次输入多个key,LPUSH/RPUSH/SADD等命令都支持一次输入多个value,也要注意每次操作数量不要过多,建议控制在50个以内;
2、PipeLine 模式 可以一次输入多个指令。redis提供一个 pipeline 的管道操作模式,将多个指令汇总到队列中批量执行,可以减少tcp交互产生的时间,一般情况下能够有10%~30%不等的性能提升,但是也需要注意操作key的数量,避免数据包拆分,控制数据包在1M以下;
3、更快的是Lua Script模式,还可以包含逻辑。redis内嵌了 lua 解析器,可以执行lua 脚本,脚本可以通过eval等命令直接执行,也可以使用script load等方式上传到服务器端的script cache中重复使用。