Redis设计规范与最佳实践
更新时间:2023-08-30
作为百度首选的kv存储解决方案,在多年使用中积累了很多最佳实践,帮助业务方最大化发挥Redis的性能优势,避免不规范不合理的使用带来影响性能的问题,我们给出如下设计使用规范,供大家参考。
Key名字如何设计?
大小:
- 建议控制在30个字节内,尽可能不超过128字节,如果是MGET超过100个数据的场景,此时Key名字最好不超过20字节。
命名:
- 不使用特殊字符,比如空格、单引号、转义字符。
- 业务标识可以当做Key的前缀,标识和含义内容用“:”分隔, 比如业务标识是“AID:15”,Key就可以写成:“AID:15:player:ranking”,方便遇到问题时根据业务标识尽快定位;如果Key本身是拆分Key,可以使用"#"分隔,比如“video_info#olympic#2020-08-29#1”,“video_info#olympic#2020-08-29#2”,提高可读性。
- 常见单词尽可能使用英文简写, 比如“economic”简写为“ec”,“file path”简写为“fp”。
- 正确使用Redis的hashtag,默认符号的配置是“{}”,集群版是按照hashtag(第一个“{”和第一个“}”之间的内容)来进行路由的,比如“a{aa{xxx}bb}b”是按照“{aa{xxx}”转发到同一个数据分片来实现分布式Redis的RENAME命令的操作,要注意使用hashtag避免大量数据落入同一个数据分片, 造成多个节点数据不均衡,不需要使用hashtag的场景,禁止使用“{}”,如果有特殊原因必须使用,建议在控制台关闭hashtag的特性。
过期
- 首先我们建议Key都是需要设置过期时间的,尤其是秒杀、热门活动等集中写入的Key的场景, 在设置过期时间的时候要加上随机值, 让过期时间分散一些。
Value如何设计?
大小以及素个数:
- String类型的数据建议百字节以内,控制在1K以下, 最好不超过10K,如果是MGET或者使用Pipeline的场景,建议小于4K,Redis限制值是每个Value最大不超过512M。
这里我们给出参考值,Redis请求最大10ms延迟级别下,对应的Key大小和QPS:
单kv大小 | 100字节 | 1K | 10K | 50K | 100K | 500K |
---|---|---|---|---|---|---|
qps | 9.7w | 7.8w | 7.3w | 6.8w | 4.2w | 6400 |
- 复合类型Hash、List、Set、Zset,默认Redis配置下,控制成员数目并且控制单个成员大小是可以对应使用压缩算法的,可以降低Redis本身数据结构的开销。
类型 | 成员个数最大值 | 成员大小最大值 |
---|---|---|
Hash | 512 | 64B |
Set | 512 | 64B |
List | 512 | 64B |
Zset | 128 | 64B |
- 那么对于Set、Hash、List这类复杂数据类型,要尽量降低数据结构中的元素个数,建议元素个数控制1000以下,单Key对应Value大小不超过1M。
这里我们给出参考值,预置单Hash Key下,每个成员长度为10字节,Field个数在10、100、500、1000、3000、5000情况下,HGETALL操作对应的平响时间、qps测试结果:
kv大小:member数量*10字节 | 10*10 | 100*10 | 500*10 | 1k*10 | 3k*10 | 5k*10 |
---|---|---|---|---|---|---|
平响时间(ms) | 0.4 | 0.7 | 2.7 | 5 | 11 | 25 |
qps | 4.8w | 1w | 2500 | 870 | 250 | 120 |
使用的命令请求遵守哪些规范?
控制数量:
- 对于单个请求批量访问数据的场景,建议限制在100个Key以内(如:MGET一次不能超过100个Key、HMGET一次不能超过100个FIELD)。
- 每个Pipeline批次下Key数量 ,建议限制在200以内,最好控制在50以内。
- 对于O(n),O(log(n))以及更高复杂度的,控制元素个数以及使用频率,比如LRANGE 0 -1,HGETALL
数据删除:
- 对于复合类型的DEL,如果是已知元素个数很多的大Key,4.0以上版本建议使用UNLINK方式删除,如果是低版本的Redis,可以使用HSCAN、SSCAN、ZSCAN。
- 对于4系以上版本Redis建议开启Redis的lazyfree配置,启用异步删除的功能
LUA 使用:
- 不建议将复杂的EVAL脚本放在Redis执行,会导致占用CPU以及内存资源,带来性能下降,我们6系的版本支持基础命令以外的MODULE来满足业务场景。
- 不建议LUA中包含事务以及redis.replication相关的操作。
- LUA脚本中变量需要保证是执行的时候给定脚本和参数,建议执行EVAL将LUA的脚本缓存在Redis中,通过EVALSHA的命令执行脚本内容。
- 执行EVALSHA收到返回是脚本不存在的时候,需要重新执行EVAL。
消息队列或者通信:
- 发布订阅的业务场景,要注意发布和订阅保持平衡, 包括订阅或者取消订阅的是已有的频道,避免出现热点。
- 一次订阅建议控制在50个频道以内,以免输出缓冲区过高触发客户端连接的断连。
- 用List类型做消息队列,要注意生产和消费保持平衡,并且通过LEN检查长度,通过LTRIM对队列的长度进行控制,及时截断。
- 当出现队列异常堆积以后,控制再次消费的速度,避免大量堆积任务同一时间消费导致输出缓冲区过高触发客户端连接的断连。
慎用的命令:
- 如果不确认Key是Big-key,不确认当前Redis的内存使用量,慎用DEBUG相关的命令(如 DEBUG DIGEST/OBJECT等),避免造成阻塞,如果需要检查Key的大小,使用4系及以上高版本MEMORY命令检查,低版本建议使用复杂度是O(1)的命令检查,比如:STRLEN,HLEN(更多详见:https://redis.io/commands/)。
- 非必要不使用MONITOR命令,如果需要,预先检查Redis的网络流量,在确保网络流量在10M/s以下,安全使用MONITOR命令。
- 非必要不使用KEYS命令,如果需要,在确保自己的数据量控制在网络流量10M/s以下,安全使用KEYS命令。
该如何设计数据结构以避开大Key和热Key?
老生常谈的大Key和热Key,需要拆、需要打散,如上,我们也给出各种类型Value建议大小,怎么拆呢?又如何打散呢? 除了拆和打散,还有没有别的办法呢?下面举一些常见的场景案例。
业务使用场景 | 举例 |
---|---|
消息队列 任务记录 |
用户任务记录(读文章,签到,看视频...),比如: Key的设计最好以时间段划分 按月记录 uid:task:202209 而非将所有任务记录到一个Key: uid:task:all ![]() |
注意事项:
- Key名字要设置合理过期时间,避免一个Key中堆积很长时间历史数据。
- 如果预期Key的成员会随着时间增加而变多,建议“HSCAN,SSCAN......”的方式定期清理。
- 消息队列要监控上下游的生产消费速度,或者监控队列长度,避免任务堆积。
业务使用场景 | 举例 |
---|---|
“可分类的数据” 地域信息 配置信息 活动种类 业务种类 |
根据考生分数查询可报考学校,比如: 将多个地域存一个Key拆开,改为多个地域对应多个Key,比如: “全国考生分数学校对应信息(key)—— 省份_分数段_政史地(field)—— 可以报考学校的信息(value)”, 可以优化成: “云南考生(key)—— 政史地:分数段(field)—— 可以报考学校的信息(value)” ![]() |
注意事项:
- 对于地域,时间,业务种类这种数据,设计Key的粒度要尽可能小,避免出现地域级别热点Key,利用分类将Key打散。
- 有时候设计Key的时候习惯用Key名字对应MySQL表名字,那这个Key的范围就容易很大,我们建议将Key拆开,用一些常用数据库字段对应到Key上。
业务使用场景 | 举例 |
---|---|
“属性相关信息” 用户信息 设备规格 聊天记录 商品规格 |
记录不同分数段对应的学校信息,比如: 上例中“edu0_info”对应“100-150分数段物化生考生”可报考所有学校的信息,那么所有学校的信息本身是超过4KB的JSON,此时不建议Hash类型Value存储JSON,可以存JSON的Key名字“edu0_info_key” ![]() |
注意事项:
- 复合类型元素或者成员的Value建议小于4k,避免HGETALL,LRANGE 0 -1,SMEMBERS 取全集导致Redis压力过大。
- 建议将HGETALL换成HMGET,将LRANGE 0 -1换成LRANGE 0 50获取,SMEMBERS换成SISMEMBER,ZRANGE 0 -1换成ZRANGEBYSCORE加LIMIT。
- 元素或者成员Value建议是字符串,而不建议使用大JSON或者数组,可以将JSON存到单独的Hash或者String类型的Key中。
业务使用场景 | 举例 |
---|---|
“各种ID” 粉丝列表 视频收藏列表 歌单歌曲列表 文章信息库 关注用户列表 |
某次活动中奖用户记录,比如: 用户ID用1000取模,就可以将用户ID全集的1个Key拆成1000个Key,如果ID很多,可以用ID和ID前2个字符取模两次,按需分段uid#0#0,uid#1#1 ,......, uid#999#99 ![]() |
注意事项:
- 利用Value在Key的名字上取模,常用在各种ID,即Value是整型,比如用户ID、书籍ID、文章ID。
- Key名字使用的分隔符号不建议使用空格和可能引起运算的符号(“-”,“.”,“+”,“=”,......),建议用 “#”,“:”,“_”。
业务使用场景 | 举例 |
---|---|
“排序场景” 音乐历史记录排序 文章取热门评论排序 歌手热门歌曲排序 |
音乐播放历史,比如: 使用ZREMRANGEBYRANK 定期控制成员个数在1000左右,减轻ZREVRANK的O(log(N))开销 ![]() |
注意事项:
- 预期会经常排序的数据,建议控制元素(成员)个数在1000以内。
- ZREMRANGEBYRANK本身复杂度为O(log(N)+M) ,注意使用频率,天级别或者整点或者业务低峰期执行为好。
业务使用场景 | 举例 |
---|---|
“一对多的关系” 作弊规则 反攻击规则限制 配置应用 权限管理 |
屏蔽黑名单用户,比如: 大Set拆成单个String类型的Key,JSON格式的大String换Hash类型的Key ![]() |
注意事项:
- 使用合理的数据类型。
业务使用场景 | 举例 |
---|---|
“热门场景” 热门直播间商品信息 热门赛事赛程信息 |
查询直播间的商品,比如: 同样的数据存多份备份,随机读一个,读不到再从DB回写 ![]() |
注意事项:
- 适合读多写少的数据。
- 最佳实践是,分布式Redis中,备份数据份数可以用Redis集群分片数评估QPS分布,份数最好是集群分片数的一倍及以上。
业务使用场景 | 举例 |
---|---|
页面 “默认” 展示 | 查询高考分数线的功能,比如: 需要显示类如【北京】【211】【北京邮电大学】,那么对于这种前端页面打开默认显示的数据不建议从Redis每次动态读取,可以业务侧本地缓存 |
注意事项:
- 如果是常用枚举,或者默认参数的数据,建议代码里保存。
业务使用场景 | 举例 |
---|---|
“大文本” 大图文 图片内容 小说 长篇文章 博客 评论内容 客服对话 |
将小说文章压缩后存储 |
注意事项:
- JSON和XML换成轻量级的Protobuf。
- 使用Snappy或GZIP压缩。
- 预期Key可能过大,灌入数据的时候,使用EXPIREAT设置过期时间,不要集中到某个时间戳,最好加上随机时间。
业务使用场景 | 举例 |
---|---|
所有场景 注意命令时间复杂度及其使用频率 |
比如O(n),O(log(n))以及更高复杂度的,控制元素个数,以及命令使用频率,时间复杂度详见:https://redis.io/commands/ 比如INCR的Key需要设置一天的过期时间,那么可以根据返回结果判断,如果已经大于1,就不需要重复 PEXPIREAT 比如Redis4系及以上可以使用MEMORY USAGE查询Key的内存占用,避免未知Key大小,执行DEBUG OBJECT 造成阻塞 比如 O(N) 的N是成员(元素)总数的慎用或者降低使用频率 |
注意事项:
列举常见又常用的高复杂度命令如下:
- List类型:LRANGE 范围取0 -1时慎用,LREM。
- Hash类型:HKEYS,HVALS,HGETALL。
- Set类型:SINTER / SINTERSTORE,SUNION / SUNIONSTORE,SMEMBERS。
- Sorted Set类型:ZRANGE(...) 范围取0 -1时慎用,ZREVRANGE(...)。ZREMRANGEBY(...),ZUNION(...)。
- Geo 类型:GEOHASH,GEORADIUS(...),GEOSEARCH(...)。
- 其他:KEYS,MONITOR,DEBUG。