redis 不是垃圾桶也不是 SUPER MAN,能力和资源都有限,不合理的使用会降低它的健康度,严重时甚至会引起 redis 抖动、阻塞等进而导致服务不可用,每一个使用 redis 的开发人员都应当掌握规范的开发和使用方法。本文整理出 redis 开发过程中七个较常出现的使用不合理的场景,并辅以案例进行分析说明。
01 合理使用集合类
案例
某活动需求,每天 10 点对昨天参加某活动的用户进行推送提醒。开发人员使用 redis 存储每天参加活动的用户,通过 ZRANGEBYSCORE 命令获取目标用户进行提醒,提醒完后使用 ZREMRANGEBYSCORE 命令从 redis 中清除这批用户。某一天 ZRANGEBYSCORE、ZREMRANGEBYSCORE 均出现了慢日志报警,排查发现这一天参加该活动的用户约有 5 万。
分析
案例中使用了 redis 的 sortedset 来储存用户信息,其中 value 是用户的账号、score 是用户参加活动的时间,由于 ZRANGEBYSCORE 和 ZREMRANGEBYSCORE 命令的时间复杂度是 O(log(N) + M),其中 M 是操作的元素个数,N 是集合元素总数,本例中当用户数量为 5 万时出现慢日志。可以通过缩小每次查询的集合数量,可以将一天分成多段,分批次查询,比如把查 24 小时范围的用户改为查 4 小时范围的用户,分别查 6 次处理即可。
Q
如果用户参加活动的时间很集中,在某一个时间段(比如晚 18 点到 22 点)查出来的数量还是特别多怎么办?
A
可以把粒度分得更细一些比如 1 小时或者 30 分钟,如果确定用户参加活动集中在某个时间点,可以考虑使用 ZSCAN 遍历操作并删除。另外,对于目标时间范围有确定的首尾元素时,还可以通过 ZRANK 命令查出元素的位置,通过 ZRANGE 以及 ZREMRANGEBYRANK 来进行查询和删除操作,这样每次操作可以控制操作数量,有效避免慢日志。
小结
使用 sortedset、set、list、hash 等集合类的 O(N)操作时要评估当前元素个数的规模以及将来的增长规模,对于短期就可能变为大集合的 key,要预估 O(N)操作的元素数量,避免全量操作,可以使用 HSCAN、SSCAN、ZSCAN 进行渐进操作。集合元素数量过大在使用过程中会影响 redis 的实际性能,元素个数建议尽量不要超过 5000,元素数量过大可考虑拆分成多个 key 进行处理。
02 合理设置过期时间
案例 1
某投票功能,用于统计今日环比昨日的增长数量,开发人员使用 redis 存储每天的投票数,key 设计为 vote_count_{date},其中{date}为当天的日期,由于没有设置过期时间,一年以后产生了 360 多个 key,实际在用的 key 始终只有 2 个。
分析
该案例中,每个生成的 key 在 2 天以后都不会再使用了,可将 key 加上过期时间。
案例 2
某统计功能,用户会不定期的导入一批数据进 redis,每一批数据需要在 30 分钟后、1 天后、3 天后、7 天后进行计算统计,统计结果发给用户。开发人员使用 redis 的同一个 sortedset 存储这些导入的数据,每天定时任务执行计算任务。由于没有清理,导致大量结束计算任务的废弃数据残留 redis。
分析
该案例中,每一批数据都有相应的生命周期,在导入的第 7 天执行完最后一次计算任务生命周期结束,由于集合里的元素不能单独设置过期时间,可在代码逻辑中对最后一次使用这批数据后进行清理操作。
小结
如果 key 没有设置超时时间,会导致一直占用内存。对于可以预估使用生命周期的 key 应当设置合理的过期时间或在最后一次操作时进行清理,避免垃圾数据残留 redis。
03 合理利用批操作命令
案例
某运营需求,需要给用户生成短链,短链由短链前缀+短码组成,根据短码找到用户对应的手机号,开发人员使用 redis hash 结构存储短码到手机号的映射。接口每次会导入 5 万个手机号。
分析
下面是开发人员的三种操作 redis 方案的伪代码
方案 1:直接使用 redis 的 HSET 逐个设置
结果:失败。redis ops 飙升,同时接口响应超时
方案 2:改用 redis 的 HMSET 一次将所有元素设置到 hash 中
结果:失败。出现 redis 慢日志
方案 3:依然使用 HMSET,只是每次设置 500 个,循环 100 次
结果:成功
对于大量频繁的 hset 操作可以使用 HMSET 替代减少 redis 操作次数同时提升处理速度,但是要考虑单次请求操作的数量,避免慢日志。
小结
在 redis 使用过程中,要正视网络往返时间,合理利用批量操作命令,减少通讯时延和 redis 访问频次。redis 为了减少大量小数据 CMD 操作的网络通讯时间开销 RTT (Round Trip Time),支持多种批操作技术:
MSET/HMSET 等都支持一次输入多个 key,LPUSH/RPUSH/SADD 等命令都支持一次输入多个 value,也要注意每次操作数量不要过多,建议控制在 500 个以内;
PipeLining 模式 可以一次输入多个指令。redis 提供一个 pipeline 的管道操作模式,将多个指令汇总到队列中批量执行,可以减少 tcp 交互产生的时间,一般情况下能够有 10%~30%不等的性能提升;
更快的是 Lua Script 模式,还可以包含逻辑。redis 内嵌了 lua 解析器,可以执行 lua 脚本,脚本可以通过 eval 等命令直接执行,也可以使用 script load 等方式上传到服务器端的 script cache 中重复使用。
04 减少不必要的请求
案例
某业务系统,当用户进入某个页面时会同时请求多个接口,每个接口都会校验用户状态是否有效,用户状态存在 redis 里并设置有过期时间,对于 key 未过期但是过期时间大于指定阈值的,需要重新设置有效时间,否则需要使用 del 命令删除掉。但是部分 key 由于过期其实已经不存在了,所以出现部分无效 del 命令。用户越多,就会有越多的无效命令。
分析
ttl 命令对于 key 不存在的情况会返回-2,若 key 不存在则不需要再调用 del 命令,可减少无效请求。
小结
redis 的所有请求对于不存在的 key 都会有输出返回,合理利用返回值处理,避免不必要的请求,提升业务吞吐量。
05 避免 value 设置过大
案例
某开发人员将一个商品集合信息序列化后用 redis 的字符串类型存储,使用的时候再反序列化成对象列表使用,大小超过 1MB,在网络传输的时候由于数据比较大会触发拆包,会降低 redis 的吞吐量。
分析
数量比较多时可以考虑改用 hash 结构存储,每一个 field 是商品 id,value 是该商品对象,如果数量较大可使用 hscan 获取。
小结
String 类型尽量控制在 10KB 以内。虽然 redis 对单个 key 可以缓存的对象长度能够支持的很大,但是实际使用场合一定要合理拆分过大的缓存项,1k 基本是 redis 性能的一个拐点。当缓存项超过 10k、100k、1m 性能下降会特别明显。关于吞吐量与数据大小的关系可见下面官方网站提供的示意图。
吞吐量与数据大小的关系
在局域网环境下只要传输的包不超过一个 MTU(以太网下大约 1500 bytes),那么对于 10、100、1000 bytes 不同包大小的处理吞吐能力实际结果差不多。
06 设计规范的 key 名
可读性
以业务名为前缀,用冒号分隔,可使用业务名:子业务名:id 的结构命名,子业务下多单词可再用下划线分隔。
举例:活动系统-人拉人红包活动-id,可命名为 ACTIVITY:INVITE_REDPACKET:001
简洁性
保证语义的前提下,控制 key 的长度,当 key 较多时,内存占用也不容忽视。
不包含转义字符
不包含空格、换行、单双引号以及其他转义字符。
07 留心禁用命令
keys、monitor、flushall、flushdb 应当通过 redis 的 rename 机制禁掉命令,若没有禁用,开发人员要谨慎使用。其中 flushall、flushdb 会清空 redis 数据;keys 命令可能会引起慢日志;monitor 命令在开启的情况下会降低 redis 的吞吐量,根据压测结果大概会降低 redis50%的吞吐量,越多客户端开启该命令,吞吐量下降会越多。
keys 和 monitor 在一些必要的情况下还是有助于排查线上问题的,建议可在重命名后在必要情况下由 redis 相关负责人员在 redis 备机使用,monitor 命令可借助 redis-faina 等脚本工具进行辅助分析,能更快排查线上 ops 飙升等问题。
总结
本文整理出的几点 redis 开发规范主要是涉及 redis 客户端的使用部分,每个开发人员在使用 redis 开发过程中几乎都会涉及到上述提到的几个问题,需要多多留心,提高代码质量,提升 redis 的健康度。
本文转载自公众号中间件小哥(ID:huawei_kevin)。
原文链接:
https://mp.weixin.qq.com/s/gw1X34vtiwRcEYgLbWp23w
评论