这是我参与「第五届青训营」伴学笔记创作活动的第17天。今天的内容是关于非关系型数据库 Redis 的使用案例和一些应用技巧的。
1 Redis 介绍
使用背景:数据量增大导致 MySQL 从单体到集群,但是满足不了秒杀等应用场景,所以希望数据存到内存中。因此将热数据(经常被访问的数据)存到内存中。
1.1 Redis 的工作原理
数据从内存中读写,数据保存在硬盘中保证重启不丢失,单线程处理所有命令。
增量数据保存到 AOF 中,全量数据保存到 RDB 文件中。
2 Redis 应用案例
2.1 案例1:连续签到
业务逻辑:用户点击签到,天数+1。连续定义为每天的0:00:00到23:59:59。
实现方法:设置过期时间为当前日期的24小时后。
涉及的 String 数据结构:
sds 数据结构:可以用来存储字符串、数字、二进制数据(满足二进制安全),通常和 expire 配合使用。常用在存储计数和 Session 场景。
在内存中,一共存储4块数据,从左到右依次是 len(value 的长度), alloc(实际分配大小,通常大于 len), flags(共8位,高5位暂时保留,低3位表示数据类型)和 value。有一个指针 sds 指向 value 的开头,向左获取元数据,向右获得值。
2.2 案例2:消息通知
使用场景:消息通知、进入推送范围。
实现方法(之一):用 list 作为消息队列。
涉及的 Quicklist 数据结构:
Quicklist 的本质:双向链表 + listpack (entry 部分,相当于 value)。在 listpack 中包含 tot-bytes(32位), num-elements(16位), element 1, element 2, ……, element N, listpack end-byte(8位,固定为255)。其中每个 element 的长度相同,通过 tot-bytes 可以知道这个 listpack 的结束位置。
为什么要有 listpack?Redis 为了节省内存,可能会在一个节点内存多个数据。
2.3 案例3:计数
涉及的 hash 数据结构:通过 hash 函数算出槽位。当一个槽位中数据链表过长时,需要将数据拷贝到下一个哈希表中。
rehash 操作:将 hashtable[0] 中的数据全部迁移到 hashtable[1] 中。在数据量小的时候直接复制,数据量大时迁移过程会明显阻塞用户请求。
渐进式 rehash:每次用户访问时都会迁移少量数据。将整个迁移过程平摊到所有的放分过程中。
2.4 案例4:排行榜
应用场景:积分变化时实时变更。
应用实现:跳表(双向链表)的倒序查询 key + dict 中查询 value。
涉及的 zset 数据结构:
zskiplist 跳表技术:可以视为将链表分为几条子链。结合字典后可以通过 key 实现跳表。通常情况下,Redis 中跳表不会超越4层。
2.5 案例5:限流
应用场景:要求1秒内放行的请求为 N 条,超过 N 则禁止访问。
解决方案:取当前时间为 key,使用 increase 方法,超过阈值禁止访问。
2.6 案例6:分布式锁
应用场景:并发(如秒杀时)。
解决方案:Redis 中的 setnx。因为 Redis 是单线程执行命令,且 setnx 只有未设置过才能执行成功。
3 Redis 使用注意事项
3.1 大 Key 和 热 Key
大 Key
- 大 Key:String 类型中 value 字节数大于 10 KB;Hash / Set / Zset / List 等复杂数据结构类型中元素个数大于5000 或 总字节数大于 10 MB。
- 大 Key 的危害(和 Redis 执行命令的过程有关):读取成本高;容易导致慢查询(过期、删除);主从复制异常、服务阻塞,无法正常响应。业务侧表现为请求 Redis 超时报错。
- 消除方法:拆分(构建多个 key,在第一个 key 存储的数据中记录拆分的段数,按照特定规则定义后续内容的 key 名)、压缩(选择合适的压缩算法对 value 压缩后再存储,通常情况下压缩率越高解压时间越长)、集合类数据结构(hash、list、set、zset)可以拆分或区分冷热(如 Redis 只缓存前10页,后续数据走 DB)
Redis 执行命令的过程
- 单条命令:读取命令,解析命令,执行命令,写入数据。
- 多条命令:读取所有命令,解析所有命令,执行所有命令,写入所有命令。
热 Key
- 热 Key:用户访问 QPS 特别高的 Key,导致 Server 实例出现 CPU 负载突增或者不均的情况。数值上没有明确标准,超过500就有可能成为热 Key。
- 解决方案:设置 localcache(在服务侧设置 localcache,降低访问 Redis 的 QPS,localcache 缓存过期或未命中时从 Redis 更新)、拆分(把 KV 对写多份,但更新时需要更新多份,可能出现短暂的数据不一致)、使用 Redis 代理的热 Key 承载能力(字节解决方案,热 Key 发现 + Localcache)
3.2 慢查询场景
容易导致 Redis 慢查询的操作:
- 批量操作一次性传入过多的 KV,如 mset, hmset, sadd, zadd 等 O(n) 操作。建议单批次不超过100。
- zset 大部分命令是 O(log n)。大小超过 5K 时,zadd, zrem 等也可能导致慢查询
- 操作单个 value 过大,超过 10 KB,即大 Key
- 对大 Key 的 delete/expire 操作也可能导致慢查询
3.3 缓存穿透和缓存雪崩
缓存穿透:热点数据绕过缓存,直接查询数据库
缓存雪崩:大量缓存同时过期
缓存穿透的危害:查询一个一定不存在的数据时会导致请求发送至 DB,如果有 bug 或人为攻击容易导致 DB 响应慢或宕机;热 Key 缓存过期时也会导致大量请求发送至 DB
避免缓存穿透:缓存空值、布隆过滤器(使用 bloom filter 算法来存储合法 Key,压缩率极高)
避免缓存雪崩:分散缓存时间(将缓存失效时间分开,例如在原有失效时间基础上增加一个随机值)、使用缓存集群(避免单机宕机造成的缓存雪崩)