8. 消息:事务、Pub/Sub、Stream、延迟队列
本页覆盖 Redis 弱事务与消息相关能力。Stream / 延迟队列见文末补强;原子组合逻辑优先 Lua · 并发。
❓ 面试官:Redis 的事务跟 MySQL 的事务有什么区别?它支持回滚吗?
频率:🔥🔥🔥
💡 一句话总结(先抛结论): Redis 的事务非常弱,跟 MySQL 的 ACID 事务完全不是一个级别。它不支持原子性的回滚(Rollback)。
📝 详细原理解析:
- 如何开启和执行:
- 客户端发送
MULTI开启事务。 - 接着发送各种读写命令(比如
SET、INCR),Redis 此时不会立刻执行,而是把这些命令放进一个队列里。 - 最后发送
EXEC,Redis 就会按顺序依次执行队列里的所有命令。
- 客户端发送
- 为什么不支持回滚?:
- 语法错误(组队时报错):如果你在
MULTI之后敲错了一个不存在的命令(比如SEET a 1),整个队列会在EXEC时被直接丢弃,所有命令都不执行。这勉强算是一种“原子性保护”。 - 运行错误(执行时报错):这是最坑的。比如你对一个 String 类型的 Key 故意执行了只支持 List 的
LPUSH命令,Redis 在组队时根本发现不了(语法是对的)。直到EXEC真正执行时,碰到LPUSH报错了,Redis 会继续往下执行队列里剩下的正确命令!而之前已经执行成功的命令,绝对不会回滚!
- 语法错误(组队时报错):如果你在
- 官方的解释:
- Redis 作者认为,支持回滚需要极其复杂的内部机制(像 MySQL 的 undo log),这会严重拖慢 Redis 简单极致的性能。而且这种“运行错误”通常是程序员写出了 Bug 造成的,不应该由数据库的底层引擎来兜底。
🌟 面试加分项(替代方案): 面试官常追问:“那如果我的业务真的强依赖多个命令的原子性操作怎么办?” 答:在现在的企业开发中,几乎没人再用 MULTI / EXEC 原生事务了。 替代方案是使用 Lua 脚本。Redis 保证在执行 Lua 脚本期间,整个服务器是阻塞的(单线程),其他任何客户端的命令都插不进来,这就完美实现了真正的“原子性”。(这也是分布式锁 Redisson 底层大量使用的技术)。
❓ 面试官:Redis 里的 Watch 机制(乐观锁)是怎么回事?【中级】
频率:🔥🔥
💡 一句话总结(先抛结论):WATCH 是 Redis 配合事务 MULTI 使用的一种乐观锁机制(CAS)。它用于在事务开启之前,监控一个或多个 Key 是否被其他客户端偷偷改动过。
📝 详细原理解析:
- 使用场景:比如两个客户端同时想执行
INCR balance(给账户余额加 1)。 - 操作步骤:
- 客户端 A 在开启事务前先执行:
WATCH balance。 - 客户端 A 接着执行
MULTI->INCR balance->EXEC。
- 客户端 A 在开启事务前先执行:
- 核心原理:
- 在 A 执行
EXEC的那一瞬间,Redis 会去检查balance这个 Key 的版本号。 - 如果发现就在 A
WATCH之后、EXEC之前这段时间里,有另一个客户端 B 偷偷改了balance的值。 - Redis 就会直接中止 A 的整个事务(
EXEC返回 nil),A 的所有命令全部失败。 - A 只能无奈地自己写个
while(true)循环重新去WATCH并重试。
- 在 A 执行
❓ 面试官:你了解 Redis 的 Pub/Sub(发布订阅)功能吗?它能当做消息队列(MQ)来用吗?【中级】
频率:🔥🔥
💡 一句话总结(先抛结论): 发布订阅是一种消息广播机制。千万不要把 Redis 的 Pub/Sub 当作企业级的消息队列来用! 因为它最大的致命伤是“发后即忘”(Fire-and-forget),消息极度容易丢失。
📝 详细原理解析:
- 怎么用?:
- 消费者打开一个终端:
SUBSCRIBE channel_1(订阅频道 1)。 - 生产者打开另一个终端:
PUBLISH channel_1 "hello"(向频道 1 发消息)。 - 消费者立刻就能收到 "hello"。
- 消费者打开一个终端:
- 为什么不能当 MQ 用?(三大致命缺陷):
- 消息丢失 1(消费者不在线):如果没有任何消费者订阅这个频道,或者消费者网络断了,你
PUBLISH的消息会被 Redis 直接丢进垃圾桶。等消费者连上线后,历史消息绝对找不回来。 - 消息丢失 2(积压问题):即使消费者在线,如果生产者发消息的速度远超消费者处理的速度,导致输出缓冲区塞满,Redis 为了保护自己不死掉,会无情地踢掉这个消费者连接,消息依然丢失。
- 没有持久化:所有的发布订阅消息只存在于内存的临时中转区,根本不会写进 RDB 或 AOF 日志里。
- 消息丢失 1(消费者不在线):如果没有任何消费者订阅这个频道,或者消费者网络断了,你
🌟 面试加分项(实战架构演进): 面试官可能会问:"既然 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 更直观。库存/限流原子逻辑见 并发。