Redis 是程序员工具箱里很容易被“过度信任”的东西。

它太顺手了。

想存个值,SET 一下。想自动过期,EXPIRE 一下。想做计数器,INCR 一下。想搞队列,LPUSH / BRPOP 一下。想搞锁,SET NX PX 一下。再看一眼 Stream,好像连消息系统也能顺手安排。

于是很多系统就这么一路滑坡:

第一天:Redis 只是缓存。
第二天:这个状态先放 Redis 吧。
第三天:队列也先放 Redis 吧。
第四天:库存扣减也先放 Redis 吧。
第五天:Redis 挂了,大家开始翻日志考古。

这篇文章想讲的观点很简单:

Redis 能做很多事,但不要让它单独拥有真相。

如果一份数据不能丢、不能错、不能重复处理、不能悄悄过期、不能被内存策略淘汰,那它就不应该只待在 Redis 里。

Redis 最舒服的位置是缓存。它可以让系统跑得更快,但不应该决定系统还活不活得明白。

先看边界:缓存可以重建,事实不能只靠 Redis

先把话说在前面:这不是 Redis 不行。

Redis 很强,而且越来越强。它有 RDB,有 AOF,有复制,有 Sentinel,有 Cluster,有 Streams,还有很多高级数据结构。Redis 8 之后,JSON、Search、Time Series、概率数据结构等能力也被合进 Redis Open Source。

所以问题不是“Redis 能不能干更多活”。

问题是:你把活交给 Redis 之后,能不能接受它的失败方式。

缓存的失败方式很朴素:

Redis 没了
  -> 缓存 miss
  -> 回源数据库
  -> 重新写缓存

这很烦,但不至于让业务当场失忆。

如果 Redis 保存的是唯一订单状态、唯一库存、唯一任务队列、唯一支付回调记录,失败路径就完全不一样:

Redis 没了
  -> 事实没了
  -> 业务开始沉默
  -> 人开始翻日志
  -> 会议开始变长

缓存丢了,可以重建。事实丢了,只能考古。

这就是 Redis 的边界:它适合做性能层,不适合做户籍科。

下面不按 Redis 命令分类,而按业务里最常见的“强行实现的功能”分类。这样更容易看清楚:问题往往不是某个命令危险,而是你让 Redis 承担了它不该单独承担的责任。

强行当数据库:持久化不等于事务和账本

很多人会说:Redis 也有持久化,开 AOF 不就行了吗?

这句话听起来像“我买了保险,所以可以不开安全带”。方向对了一半,风险还在。

Redis 常见持久化有两类:

  • RDB:定期生成内存快照。
  • AOF:把写命令追加到日志里,再用日志恢复。

它们都很有价值,但它们解决的是“Redis 重启后尽量恢复”,不是“Redis 从此拥有数据库级语义”。

RDB 很像定期拍照。拍完照之后发生的事,如果还没来得及拍下一张,故障时就可能不在照片里。

AOF 更耐用一些,但也不是魔法。Redis 官方文档说明,AOF 可以配置不同的 fsync 策略:不主动 fsync、每秒 fsync、每次写入都 fsync。默认每秒 fsync 性能很好,但理论上可能丢最近一秒的数据。

如果 Redis 是缓存,这通常没什么。

最近一秒缓存丢了
  -> 再查数据库
  -> 再写缓存
  -> 继续上班

如果 Redis 是唯一事实来源,这一秒可能就很贵:

  • 一秒内的订单状态丢了。
  • 一秒内的库存扣减丢了。
  • 一秒内的任务入队丢了。
  • 一秒内的积分变更丢了。

当然,你可以把 AOF 调成每次写入都 fsync。但这时 Redis 最香的低延迟会被磁盘同步拖住,而且你仍然没有传统数据库里的完整事务、约束、查询、审计和恢复工具。

这就是第一个信号:

如果你为了让 Redis 像数据库,开始把 Redis 调得不像 Redis,那多半是架构位置放错了。

强行当状态仓库:淘汰策略会删掉业务事实

Redis 有一个很“缓存味”的能力:内存到上限后,可以按策略淘汰 key。

对缓存来说,这很合理。缓存本来就是副本,内存不够了,删掉一部分,最多导致更多请求回源。

Redis 官方文档讲 key eviction 时也是这个语境:Redis 常用于缓存,缓存项是持久数据的副本,所以内存不够时通常可以安全淘汰,之后再缓存回来。

但如果 Redis 里放的是业务事实,淘汰策略就从“自动清理”变成“随机心跳加速”。

假设你把用户会话、限流状态、订单中间态、任务队列都塞进同一个 Redis 实例,然后配置了 allkeys-lru

内存满了
  -> Redis 开始淘汰 key
  -> 被淘汰的可能不是缓存
  -> 而是你昨晚刚上线的业务状态

系统表现会非常有节目效果:

  • 用户突然掉登录。
  • 任务凭空消失。
  • 限流状态被清空。
  • 幂等记录没了,重复请求又能进来了。
  • 排查半天,最后发现“它只是被淘汰了”。

你也可以说:那我配置 noeviction,不让它删。

可以。但内存满了以后,新的写入命令会报错。缓存写失败通常还能接受,业务状态写失败就必须有完整的错误处理、重试、告警和补偿。

所以 Redis 的内存模型其实一直在提醒你:

我适合保存可以丢的数据。
如果不能丢,请先给它找个更稳的家。

强行当消息队列:push/pop 只是入口

Redis List 做队列太顺手了:

LPUSH jobs payload
BRPOP jobs 0

看起来已经有内味了:生产者塞任务,消费者拿任务,跑起来还很快。

但生产级队列不是“把东西排成一队”这么简单。真正麻烦的是:

  • 消费者拿到任务后崩了,消息去哪了?
  • 处理失败要不要重试?
  • 重试几次算死信?
  • 消费进度谁记录?
  • 重复消费怎么幂等?
  • 积压太大怎么削峰?
  • 消息要保留多久?
  • 以后想回放怎么办?

Redis List 不替你回答这些问题。它只是说:我能 push,我能 pop,剩下你自己想。

Redis Streams 比 List 更完整。它有消息 ID、消费者组、待确认消息、XACK,也支持修剪。Redis 官方文档也把 Streams 描述成适合有序事件流、消费者组和可配置保留的场景。

但这里仍然要看需求。

如果只是轻量异步处理,Redis Streams 可以用。如果任务可以从数据库重新扫描出来,Redis 做加速也可以。如果丢一小段消息只是少算几次统计,问题也不大。

但如果你的需求是:

不能丢
要长期保留
要跨服务大规模消费
要清晰回放
要复杂订阅
要稳定分区扩展

那就别强行让 Redis 扮演 Kafka、RabbitMQ、NATS JetStream 或 Pulsar。

专业消息系统把“消息的一生”当主线设计;Redis 的主线是内存数据访问。让 Redis 客串可以,让它长期主演就容易加班。

强行当分布式锁:拿到锁不等于拿到修改权

Redis 锁的入门写法很漂亮:

SET lock:order:1 token NX PX 30000

key 不存在才设置,还带过期时间。第一眼看上去,很有安全感。

但分布式锁难的从来不是“抢锁”,而是“锁失效之后,世界是不是还正常”。

比如:

  1. 线程 A 拿到锁,准备处理订单。
  2. A 卡住了,可能是 GC、网络抖动、机器负载飙高。
  3. 锁过期了,线程 B 又拿到锁。
  4. A 恢复执行,继续写数据库。
  5. B 也在写数据库。

这时 Redis 没有做错。锁确实过期了,B 也确实拿到了锁。

错的是业务把“我拿到了 Redis 锁”理解成“我拥有宇宙唯一修改权”。

如果一段逻辑真的不能并发,最后的保护通常应该落在事实数据所在的地方:

  • 数据库唯一约束。
  • 乐观锁版本号。
  • 状态机条件更新。
  • 幂等表。
  • fencing token。

Redis 锁可以减少冲突,可以让系统少打架,但它不应该替数据库约束站最后一班岗。

一句话:Redis 锁适合协调,不适合当法律。

强行当库存和余额系统:计数器不是流水账

Redis 的 INCR 是快乐源泉:

INCR article:1:view_count

浏览量、点赞数、曝光次数、临时统计,这些很适合 Redis。偶尔丢一点、晚点落库、事后修正,业务通常能接受。

但如果这个数字代表库存、余额、积分、抽奖名额,事情立刻从“快乐源泉”变成“凌晨告警源泉”。

库存不是一个数字。库存是一串问题:

  • 谁扣的?
  • 对应哪笔订单?
  • 支付超时怎么释放?
  • 订单取消怎么回滚?
  • 重复请求怎么防?
  • 数据库里的库存怎么对账?
  • 故障恢复后哪个值才是真的?

如果 Redis 是唯一库存来源,故障恢复时很容易进入这种场景:

Redis 里库存是 0
数据库里库存是 5
AOF 恢复后库存是 3
订单系统说卖完了
支付系统说还有未完成订单
运营说你们先别说话

这时你缺的不是更快的 DECR,而是能解释每一次变化来源的事实系统。

比较稳的姿势是:

  • Redis 保存可丢弃的库存缓存或预热计数。
  • 数据库保存最终库存和订单状态。
  • 扣减要有幂等记录和状态流转。
  • Redis 异常时可以回到数据库路径,最多变慢,不应该变错。

Redis 可以帮你扛峰值,但不要让它独自背锅。

强行当流程状态机:Session 不是业务抽屉

很多系统把 session 放 Redis,这是常见做法。

如果 session 的含义是“登录态缓存”,丢了最多重新登录,那问题不大。用户不开心,但系统还能解释。

问题是,有些 session 会被越塞越胖,最后变成业务垃圾抽屉:

  • 未提交表单。
  • 临时订单。
  • 风控决策。
  • OAuth 授权中间态。
  • 支付跳转状态。
  • 多步骤流程进度。

这些东西一旦丢失,用户可能无法继续流程,业务也可能不知道下一步该怎么走。

判断方法很简单:

Redis 清空后,用户重新登录或重新操作就能恢复吗?

如果能,它像缓存。

如果不能,它只是伪装成 session 的业务事实。

强行当大对象存储:一个 key 不该包办所有业务关系

Redis 快的前提,是命令尽量短小,key 尽量克制。

把 Redis 当数据库用,最后很容易长出大 key:

  • 一个 Hash 存几百万个用户状态。
  • 一个 ZSet 存全站排行榜和所有历史分数。
  • 一个 Stream 从不修剪。
  • 一个 Set 保存所有活跃用户且长期不拆。
  • 一个 JSON 文档越塞越大。

刚开始你会觉得很方便:

一个 key 搞定,查询也简单,完美。

过一阵子它会开始还账:

  • 删除可能阻塞。
  • 迁移变慢。
  • RDB/AOF 变重。
  • 复制压力变大。
  • 网络返回太大。
  • Cluster 下单个 slot 过热。

大 key 最烦的地方是,它通常不是一天长大的。它像需求堆出来的历史包袱,刚发现时已经不太敢动。

缓存里的大 key 还好办,能拆、能过期、能重建。

数据库化的 Redis 大 key 就尴尬了:你想删它,它说里面有业务;你想迁它,它说我很大;你想拆它,它说先开个排期。

强行当搜索和审计系统:能查不等于能追责

还有一类常见滑坡,是把 Redis 当成搜索、复杂查询或审计日志系统。

这类需求一开始也很容易被 Redis 的数据结构吸引:

  • Set 能做集合关系。
  • ZSet 能按分数排序。
  • Hash 能挂很多字段。
  • Stream 能保存事件。
  • Redis 8 之后还合进了 Search、JSON、Time Series 等能力。

所以很多方案会从一句很自然的话开始:

先用 Redis 存一下,查起来快。

但搜索和审计的核心问题通常不是“能不能查到”,而是:

  • 查询条件会不会越来越复杂?
  • 索引如何构建、更新和回滚?
  • 数据需要保留多久?
  • 谁改过,什么时候改的,为什么改的?
  • 故障恢复后能不能重放和校验?
  • 出问题时能不能给业务、财务或风控解释清楚?

如果只是做搜索结果缓存、热榜缓存、短期查询加速,Redis 很合适。它负责快,事实来源仍然在数据库、搜索引擎、对象存储或日志系统里。

如果你想让 Redis 单独承担“可查询、可追溯、可恢复、可审计”的完整责任,就已经不是缓存问题了。看到这里,问题就变成了:你到底需要一个快的副本,还是一个能长期解释事实的系统?

按功能判断:Redis 能参与,但别单独负责

不要问“Redis 能不能做这个”。

Redis 的答案经常是:能。

更应该问的是:

Redis 出问题时,我能不能接受后果?
功能分类Redis 适合承担什么更稳的做法
读取缓存适合做商品详情、热点配置等读取加速数据库或配置中心为准,Redis miss 后回源
临时计数适合做浏览量、曝光、临时统计的聚合缓冲异步落库,可容忍延迟和修正
登录态只适合可重新登录的会话缓存丢失后能让用户重新认证,而不是丢业务流程
订单状态机不适合做唯一事实来源数据库状态机 + Redis 缓存
库存、余额、积分账本不适合做唯一账本数据库事务、幂等记录和流水为准,Redis 做预热或限流
可靠任务队列适合轻量异步,谨慎做主链路Kafka/RabbitMQ/NATS,或数据库任务表
分布式锁正确性适合协调,不适合兜底数据库约束、版本号、状态条件更新、fencing token
搜索和复杂查询适合做结果缓存或专门索引的一部分搜索引擎、数据库索引,或明确维护 Redis Search 的索引生命周期
审计日志不适合做唯一记录追加日志、对象存储、数据库、消息系统

这张表背后的原则只有一句:

Redis 可以参与流程,但不要让它单独拥有真相。

正确分工:Redis 坐性能层这桌

一个更稳的架构通常长这样:

读请求
  -> 先读 Redis
  -> miss 后读数据库
  -> 写回 Redis

写请求
  -> 先写数据库或消息系统
  -> 再删除/更新 Redis

这里 Redis 的价值很明确:

  • 降低数据库读压力。
  • 扛住热点访问。
  • 保存短期状态。
  • 做可丢弃的聚合。
  • 做限流、去重、短期幂等等辅助能力。

但系统真相在别处:

  • 订单在数据库。
  • 消息在消息系统。
  • 搜索索引可以重建。
  • 审计日志在持久日志系统。
  • 库存变化有流水。

这样设计的好处是:Redis 出问题时,系统最多变慢、缓存命中率下降、部分临时状态失效,但不会直接失去解释业务事实的能力。

这就是 Redis 最舒服的位置:跑得快,但不背最终责任。

可以越界的情况:前提是有兜底

工程里没有绝对规则。Redis 也不是只能做 GET / SET 缓存。

下面这些场景可以考虑越界:

  • 数据本来就是临时的,比如验证码、短期 token、限流窗口。
  • 数据可以从别处重建,比如排行榜缓存、搜索结果缓存。
  • 业务能接受丢失,比如实时在线人数、曝光计数。
  • Redis 只是第一层,后面还有数据库、日志或消息系统兜底。
  • 你非常清楚 Redis 的持久化、复制、故障转移和内存策略,并且写过恢复预案。

真正危险的是这些熟悉的话:

  • “这个数据暂时先只放 Redis。”
  • “AOF 开着,应该不会丢。”
  • “反正 Redis 很快,先把队列放进去。”
  • “库存先扣 Redis,后面再同步数据库。”
  • “这个 Hash 虽然大,但查起来挺方便。”

这些话一般不是方案成熟的标志,而是系统正在把风险打包塞进 Redis,然后给包裹贴上“以后再说”。

结尾:Redis 是加速器,不是户口本

Redis 最好的状态,是让系统更快,而不是让系统更脆。

当 Redis 是缓存时,它坏了,系统还能回源;它慢了,系统还能降级;它清空了,数据还能重建。

当 Redis 是唯一数据源时,它坏了,系统就要开始解释为什么事实丢了。

所以我更愿意把 Redis 放在这个位置:

Redis 是性能层,不是真相层。
Redis 是副本,不是账本。
Redis 是加速器,不是最后的存储承诺。

只要守住这条线,Redis 就非常好用。

一旦越过这条线,Redis 的每一个优点,都会在故障恢复时变成一句灵魂拷问:

这么重要的数据,当初为什么只放 Redis?

参考资料