4. 内存数据库状态管理:Redis的数据结构选择与淘汰机制
聊到多级存储架构,Redis 是绕不开的一环。它处在内存这一层,速度极快,但容量有限、成本高。说白了,你得把最热、最频繁访问的数据放进去,还得管好它的“生老病死”。
这一章,我重点讲三个最常用的数据结构——String、Hash、ZSet,以及两个核心机制:过期策略和内存淘汰。这些都是我在线上环境里踩过坑、调过优的东西,希望能帮你少走弯路。
4.1 数据结构选型:用对地方才是高手
很多人觉得 Redis 就是个 key-value 缓存,随便用 String 存就完了。其实不然。选错数据结构,轻则浪费内存,重则拖垮性能。我见过一个项目,用 String 存用户信息,每个字段一个 key,结果内存爆炸,查询还得拼 key,惨不忍睹。
4.1.1 String:最基础,但别滥用
String 是 Redis 最基础的类型,能存字符串、数字、甚至序列化后的对象。它简单、高效,但有个致命缺点:无法部分更新。
举个例子,你存一个用户对象:
# 不推荐:整个对象序列化成一个 String
SET user:1001 "{'name':'张三','age':30,'city':'北京'}"
# 想改 age?你得先 GET 出来,反序列化,改完再 SET 回去
# 并发下还会丢数据
# 推荐:拆成多个 key,或者用 Hash
SET user:1001:name "张三"
SET user:1001:age 30
SET user:1001:city "北京"
我个人习惯,String 只用来存三类东西:
- 独立的值:比如验证码、、计数器
- 简单的缓存:比如 HTML 片段、配置项
- 分布式锁:配合 SETNX 和过期时间
4.1.2 Hash:对象存储的首选
Hash 是 Redis 里我最喜欢的数据结构。它像一个小型的 map,适合存对象。你可以单独读写某个字段,不用动整个对象。
# 用 Hash 存用户信息
HSET user:1001 name "张三" age 30 city "北京"
# 只改 age
HSET user:1001 age 31
# 只查 name
HGET user:1001 name
Hash 的优势很明显:
- 节省内存:相比 String 存整个 JSON,Hash 的编码方式更紧凑
- 支持部分更新:改一个字段不影响其他字段,并发安全
- 批量操作:HGETALL、HMGET 一次拿多个字段
但要注意,Hash 的字段数不宜过多。我记得 Redis 官方建议,一个 Hash 里的字段数最好控制在 1000 以内。超过这个数,ziplist 编码会退化成 hashtable,内存和性能都会下降。
4.1.3 ZSet:排序与排行榜的利器
ZSet(有序集合)是 Redis 里最“聪明”的数据结构。它每个元素都带一个 score(分数),Redis 会自动按 score 排序。你想想看,排行榜、延迟队列、限流滑动窗口,这些场景用 ZSet 简直不要太爽。
# 排行榜:按分数排序
ZADD leaderboard 100 "张三"
ZADD leaderboard 200 "李四"
ZADD leaderboard 150 "王五"
# 获取前三名
ZREVRANGE leaderboard 0 2 WITHSCORES
ZSet 的核心操作:
- ZADD:添加或更新元素
- ZRANGE / ZREVRANGE:按分数范围取数据
- ZRANK / ZREVRANK:获取排名
- ZREM:删除元素
我在项目中遇到过一个问题:用 ZSet 做延迟队列,score 存时间戳,消费者轮询 ZRANGEBYSCORE 取到期的任务。但数据量一大,ZSet 的插入和删除性能会下降。嗯,这里要注意,ZSet 底层是跳表 + 哈希表,时间复杂度是 O(log N),N 越大越慢。所以,ZSet 的容量最好控制在百万级以内。
4.2 过期策略:让数据自动“消失”
Redis 的过期策略,说白了就是怎么把过期的 key 删掉。很多人以为 Redis 会实时扫描所有 key,发现过期就删。其实不是,那样太费 CPU 了。
Redis 用了两种策略配合:
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 惰性删除 | 访问 key 时,检查是否过期,过期则删除 | 不浪费 CPU | 过期 key 可能一直占用内存 |
| 定期删除 | 每隔 100ms 随机抽查一批 key,删除过期的 | 避免内存无限增长 | 可能漏删,需要配合淘汰机制 |
这两种策略是互补的。惰性删除保证“用的时候一定干净”,定期删除保证“不用的时候也不会一直占着”。但定期删除是随机的,如果过期 key 太多,可能一次没扫完,下次又扫别的去了。这时候,就需要内存淘汰机制来兜底。
4.3 内存淘汰机制:最后的防线
当 Redis 内存用满时,就会触发淘汰机制。这就像你家冰箱塞满了,必须扔掉一些东西才能放新的。Redis 提供了 8 种淘汰策略,我挑几个最常用的说说。
| 策略 | 含义 | 适用场景 |
|---|---|---|
| noeviction | 不淘汰,写操作直接报错 | 数据不能丢的场景 |
| allkeys-lru | 从所有 key 中淘汰最近最少使用的 | 通用缓存,最常用 |
| volatile-lru | 从设置了过期时间的 key 中淘汰最近最少使用的 | 缓存 + 持久化混合使用 |
| allkeys-random | 随机淘汰任意 key | 数据访问频率均匀的场景 |
| volatile-ttl | 淘汰即将过期的 key(剩余 TTL 最短的) | 优先保留“新鲜”数据 |
我个人习惯,纯缓存场景用 allkeys-lru。它最公平,谁用得少就淘汰谁。如果 Redis 里既有缓存数据,又有持久化数据(比如用 RDB/AOF 备份的),那就用 volatile-lru,只淘汰缓存,不碰持久化数据。
4.4 知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到,数据结构选型决定了“怎么存”,过期策略决定了“什么时候删”,淘汰机制决定了“内存满了怎么办”。三者缺一不可。
4.5 实战建议
最后,我总结几条实战建议,都是真金白银换来的教训:
- 数据结构选型要“对号入座”:对象用 Hash,排行榜用 ZSet,简单值用 String。别图省事一把梭。
- 过期时间一定要设:除非数据永远不删,否则每个 key 都设个过期时间。我习惯设 1-7 天,具体看业务。
- 淘汰策略选 allkeys-lru:这是最通用的方案。如果 Redis 里混了持久化数据,改用 volatile-lru。
- 监控内存使用率:当内存使用超过 80% 时,就要考虑扩容或优化了。别等到 100% 才动手。
- 大 key 要拆分:超过 10MB 的 key,要么拆成多个小 key,要么用其他存储(比如 SSD)。Redis 不适合存大对象。