Skip to content

8. 消息:事务、Pub/Sub、Stream、延迟队列 ​

本页覆盖 Redis 弱事务与消息相关能力。Stream / 延迟队列见文末补强;原子组合逻辑优先 Lua · 并发。


❓ 面试官:Redis 的事务跟 MySQL 的事务有什么区别?它支持回滚吗? ​

频率:🔥🔥🔥

💡 一句话总结(先抛结论): Redis 的事务非常弱,跟 MySQL 的 ACID 事务完全不是一个级别。它不支持原子性的回滚(Rollback)。

📝 详细原理解析:

  1. 如何开启和执行:
    • 客户端发送 MULTI 开启事务。
    • 接着发送各种读写命令(比如 SET、INCR),Redis 此时不会立刻执行,而是把这些命令放进一个队列里。
    • 最后发送 EXEC,Redis 就会按顺序依次执行队列里的所有命令。
  2. 为什么不支持回滚?:
    • 语法错误(组队时报错):如果你在 MULTI 之后敲错了一个不存在的命令(比如 SEET a 1),整个队列会在 EXEC 时被直接丢弃,所有命令都不执行。这勉强算是一种“原子性保护”。
    • 运行错误(执行时报错):这是最坑的。比如你对一个 String 类型的 Key 故意执行了只支持 List 的 LPUSH 命令,Redis 在组队时根本发现不了(语法是对的)。直到 EXEC 真正执行时,碰到 LPUSH 报错了,Redis 会继续往下执行队列里剩下的正确命令!而之前已经执行成功的命令,绝对不会回滚!
  3. 官方的解释:
    • Redis 作者认为,支持回滚需要极其复杂的内部机制(像 MySQL 的 undo log),这会严重拖慢 Redis 简单极致的性能。而且这种“运行错误”通常是程序员写出了 Bug 造成的,不应该由数据库的底层引擎来兜底。

🌟 面试加分项(替代方案): 面试官常追问:“那如果我的业务真的强依赖多个命令的原子性操作怎么办?” 答:在现在的企业开发中,几乎没人再用 MULTI / EXEC 原生事务了。 替代方案是使用 Lua 脚本。Redis 保证在执行 Lua 脚本期间,整个服务器是阻塞的(单线程),其他任何客户端的命令都插不进来,这就完美实现了真正的“原子性”。(这也是分布式锁 Redisson 底层大量使用的技术)。


❓ 面试官:Redis 里的 Watch 机制(乐观锁)是怎么回事?【中级】 ​

频率:🔥🔥

💡 一句话总结(先抛结论):WATCH 是 Redis 配合事务 MULTI 使用的一种乐观锁机制(CAS)。它用于在事务开启之前,监控一个或多个 Key 是否被其他客户端偷偷改动过。

📝 详细原理解析:

  1. 使用场景:比如两个客户端同时想执行 INCR balance(给账户余额加 1)。
  2. 操作步骤:
    • 客户端 A 在开启事务前先执行:WATCH balance。
    • 客户端 A 接着执行 MULTI -> INCR balance -> EXEC。
  3. 核心原理:
    • 在 A 执行 EXEC 的那一瞬间,Redis 会去检查 balance 这个 Key 的版本号。
    • 如果发现就在 A WATCH 之后、EXEC 之前这段时间里,有另一个客户端 B 偷偷改了 balance 的值。
    • Redis 就会直接中止 A 的整个事务(EXEC 返回 nil),A 的所有命令全部失败。
    • A 只能无奈地自己写个 while(true) 循环重新去 WATCH 并重试。

❓ 面试官:你了解 Redis 的 Pub/Sub(发布订阅)功能吗?它能当做消息队列(MQ)来用吗?【中级】 ​

频率:🔥🔥

💡 一句话总结(先抛结论): 发布订阅是一种消息广播机制。千万不要把 Redis 的 Pub/Sub 当作企业级的消息队列来用! 因为它最大的致命伤是“发后即忘”(Fire-and-forget),消息极度容易丢失。

📝 详细原理解析:

  1. 怎么用?:
    • 消费者打开一个终端:SUBSCRIBE channel_1(订阅频道 1)。
    • 生产者打开另一个终端:PUBLISH channel_1 "hello"(向频道 1 发消息)。
    • 消费者立刻就能收到 "hello"。
  2. 为什么不能当 MQ 用?(三大致命缺陷):
    • 消息丢失 1(消费者不在线):如果没有任何消费者订阅这个频道,或者消费者网络断了,你 PUBLISH 的消息会被 Redis 直接丢进垃圾桶。等消费者连上线后,历史消息绝对找不回来。
    • 消息丢失 2(积压问题):即使消费者在线,如果生产者发消息的速度远超消费者处理的速度,导致输出缓冲区塞满,Redis 为了保护自己不死掉,会无情地踢掉这个消费者连接,消息依然丢失。
    • 没有持久化:所有的发布订阅消息只存在于内存的临时中转区,根本不会写进 RDB 或 AOF 日志里。

🌟 面试加分项(实战架构演进): 面试官可能会问:"既然 Pub/Sub 这么废,那 Redis 5.0 为什么又出了个 Stream 数据结构?" 绝杀回答: "Stream 才是 Redis 官方推出的真正用来做消息队列的终极武器! 它不仅支持消费组(Consumer Group)、支持消息持久化(存入 RDB/AOF 不丢失)、支持类似 Kafka 的 ACK 确认机制。 但即便如此,在真正的海量高并发业务(如订单交易流转)中,我们依然推荐使用专业的 RabbitMQ 或 RocketMQ。Redis Stream 更适合中小规模、轻量级的异步解耦任务。"


延迟队列 ​

常见做法:用 ZSet,score = 执行时间戳;后台轮询 ZRANGEBYSCORE 取出到期任务。

  • 优点: 实现简单,和业务共用 Redis
  • 注意: 要多实例抢占(可用锁)、失败重试、积压监控
  • 边界: 强可靠延迟任务仍优先专业 MQ 延迟插件 / 时间轮服务

Stream 做轻量队列;延迟场景 ZSet 更直观。库存/限流原子逻辑见 并发。