Redis 的价值不在于”快”本身,而在于它把一组精心挑选的数据结构放在了内存里,并且保证了单个命令的原子性。用好 Redis 的关键,是让业务模型尽量贴合这些结构的原生语义——一旦发现需要在客户端把多个命令拼成一个”复合操作”,就该停下来想想选型是不是出了问题。
一、五种基础类型怎么用
String 是最简单的类型,但能力不止存字符串。它可以存整数做计数器,也可以序列化整个对象存成 JSON(慎用,见后文大 key 问题)。
# 页面浏览计数
INCR article:1001:views
# 带过期的幂等令牌
SET order:dedupe:20261021:1001 1 NX EX 300
SET key value NX EX seconds 这个组合在分布式锁、防重复提交、幂等去重里出现频率极高:NX 保证只成功一次,EX 保证即使客户端崩溃锁也能自动释放。注意这是”尽力而为”的锁,对正确性要求极高的场景还需要看门狗续期和 fencing token 之类的补强。
Hash 适合存对象的部分字段,读写单个字段不需要序列化整个对象。
HSET user:1001 name "tom" level 3 last_login 1761024000
HINCRBY user:1001 level 1
HGET user:1001 name
用户资料、商品属性这类”字段多但每次只改一两个”的场景,Hash 比 String 存 JSON 更省带宽和 CPU。字段数上千时要考虑拆分,因为小 hash 在字段少时用 ziplist 紧凑编码,字段多了会转成 hashtable,内存占用有台阶式变化。
List 是双向链表语义,常做轻量级队列和时间线。
LPUSH notify:user:1001 '{"type":"sys","id":9001}'
LTRIM notify:user:1001 0 199 # 只保留最近 200 条
RPOP notify:user:1001 # 消费端取一条
List 当队列有个硬伤:消费者取走消息后如果处理失败,消息就丢了。BRPOPLPUSH 到一个备份列表可以缓解,但需要消费者自己确认删除。对可靠性有要求的场景,更常见的做法是用专业的消息队列,List 只做”通知条数”这类可丢弃的场景。
Set 提供去重和集合运算,标签、共同关注、抽奖去重都很好用。
SADD tag:article:1001 "redis" "cache" "db"
SINTER tag:article:1001 tag:article:1002 # 两篇文章的共同标签
SISMEMBER lottery:20261021 1001 # 是否已参与
SRANDMEMBER lottery:20261021 10 # 随机抽 10 个
ZSet(有序集合)是 Redis 里最能打的结构:每个成员带一个分值,按分值排序,支持范围查询和排名。
ZADD leaderboard 98.5 "user:1001" 87.2 "user:1002"
ZREVRANGE leaderboard 0 9 WITHSCORES # 前十名
ZSCORE leaderboard "user:1001" # 查分数
ZREVRANK leaderboard "user:1001" # 查排名
排行榜、延时队列(分值存执行时间戳,配合 ZRANGEBYSCORE 取到期任务)、滑动窗口限流(分值存时间戳,窗口外的先 ZREMRANGEBYSCORE 清掉再数数量),都是 ZSet 的经典应用。
二、过期、内存与淘汰策略
Redis 的过期删除是惰性加定期的组合:访问到已过期的 key 时删除,同时后台周期性抽样检查。这意味着过期不等于立刻释放内存,用大量短命 key 时内存曲线会有滞后。
内存达到 maxmemory 时的行为由淘汰策略决定,常见几种:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| noeviction | 不淘汰,写入报错 | 数据不能丢,宁可拒绝写 |
| allkeys-lru | 所有 key 里淘汰最近最少使用的 | 纯缓存 |
| allkeys-lfu | 淘汰访问频率最低的 | 访问分布倾斜明显的缓存 |
| volatile-lru/ttl | 只在设置了过期时间的 key 里淘汰 | 混合存储,缓存与持久数据共存 |
缓存用途选 allkeys-lru 或 allkeys-lfu 是常规做法;如果实例里既有缓存数据又有不能丢的数据(比如分布式锁的 key),就要用 volatile-* 系列并给缓存 key 统一设置过期时间,避免把锁 key 挤掉。
三、key 设计规范
key 是 Redis 里唯一的索引,设计得好坏直接影响运维和排障。几个被验证过的习惯:
- 命名带上业务语义和对象 ID,用冒号分隔:
user:1001:profile、article:1001:views。这样用SCAN配合MATCH前缀能做业务级巡检和清理。 - key 不要太长。key 本身就占内存,百万级 key 时命名冗余会放大占用。语义要够,长度要省,常见做法是缩写模块名。
- 统一过期策略。能设过期的 key 一律设过期,别让”临时数据”变成永久数据。
- 用
SCAN而不是KEYS做遍历。KEYS是 O(N) 且阻塞单线程,生产环境执行等于给自己制造一次故障。SCAN是游标式增量遍历,可能重复但不阻塞。
四、大 key 与热 key
大 key 指单个 key 的 value 过大,比如一个 Hash 存了几十万个字段,或者一个 List 塞了几百万元素。危害是多方面的:读写单个大 key 的耗时会长到阻塞其他请求(Redis 单线程处理命令),删除大 key 会造成长时间阻塞甚至超时,主从同步时大 key 传输会造成延迟尖峰。
预防比治理容易:拆分(把 user:1001:history 拆成按日期分页的多个 key)、设上限(List 用 LTRIM 限制长度)、避免用 Redis 存大对象(该放对象存储的就别塞 Redis)。已经存在的大 key,用 UNLINK(异步删除)替代 DEL,至少不会阻塞请求。
热 key 是访问集中在少数 key 上,单实例带宽或 CPU 被打满。识别手段是 --hotkeys(需要 LFU 策略)或客户端埋点统计。缓解方式包括本地缓存挡住大部分读、把热 key 复制成多份(user:1001:profile:0 到 :3,读时随机取一个)、以及从业务上减少对单点的依赖。
五、Pipeline、事务与 Lua
Redis 的单条命令是原子的,但多条命令不是。需要一次执行多条命令时有三个层次的选择:
Pipeline 解决的是网络往返开销,不是原子性。把多个命令打包发过去,省掉多次 RTT,适合批量写入。批量 HSET 一万个字段,Pipeline 比逐条发送快一个数量级。
MULTI/EXEC 事务提供原子执行,但它是”批量顺序执行”,不支持回滚,也不支持命令间的条件判断。事务里的命令排队时不会执行,其他客户端的命令会插进来——它保证的是这一批命令连续执行完,不被其他命令打断。
Lua 脚本提供真正的原子性加可编程性:脚本执行期间不会有其他命令插入,脚本里可以做条件判断和循环。计数器限流、库存扣减(判断余额够不够再扣)这类”读-判断-写”的复合操作,Lua 是标准解法。
-- 库存扣减的原子操作示意
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
redis.call('DECR', KEYS[1])
return 1
end
return 0
调用用 EVALSHA 加脚本缓存,避免每次传脚本内容。要注意脚本里别做耗时操作,单个脚本执行时间过长同样会阻塞整个实例。Redis 7 之后引入的 Function 机制把脚本管理做得更规范,可以注册后按名字调用,适合团队共享脚本。
六、持久化对业务的影响
Redis 提供 RDB 和 AOF 两种持久化,都是快照式或追加式的内存数据落盘,特点差别很大。
RDB 是某个时间点的全量内存快照,文件紧凑、恢复快,适合做备份和容灾。代价是两次快照之间的数据可能丢失,而且 BGSAVE 时会 fork 子进程,数据集大时 fork 本身的耗时和写时复制带来的内存开销都值得留意。
AOF 记录写命令,丢失窗口更小(按 appendfsync 策略,always 每条命令刷盘、everysec 每秒刷盘、no 交给操作系统)。always 最安全但性能代价明显,everysec 是常见的折中。AOF 文件会持续膨胀,需要定期重写(BGREWRITEAOF)把重复命令压缩掉。
对业务的影响主要在两处:一是持久化开销高峰时段(fork、AOF 刷盘)请求延迟会抖动,压测和容量规划要把这部分算进去;二是恢复时间决定故障切换的时长,RDB 快、AOF 文件大时回放慢。多数生产配置是 AOF 开启加 everysec,配合 RDB 做定期备份,兼顾恢复速度和丢失窗口。
选型时先想清楚一件事:这份数据丢了能不能从别处重建?能重建的缓存数据可以不持久化或用宽松策略,不能重建的(比如计数器、去重集合)就必须认真对待持久化参数和备份。这个判断比纠结 appendfsync 取哪个值更重要。
Comments (0)
No comments yet — be the first.
Please Sign in to join the discussion.