事务
核心意义
Redis 事务的主要作用,是把多条命令"打包"为一组,然后按照顺序执行。它解决的核心问题不是像 MySQL 那样提供完整的强事务能力,而是在执行这一组命令时,尽量避免其他客户端的命令插入到这组操作中间。
可以先用一个生活例子理解。
你和朋友约好晚上去吃烧烤。你先到了烧烤店,点了牛肉串、羊肉串、五花肉等,但是你告诉服务员:
"我人还没齐,你先把单子记下来,先别急着烤。"
过了一会儿,朋友到了,又加了一些主菜和烤腰子。最后你说:
"好了,开始烤吧。"
这个过程里,前面点的和后面追加的菜,会被当成同一组订单一起处理。重点是:这组订单中间不会被别人插队。
对应到 Redis 事务里也是类似的:
MULTI 开启事务
命令1 进入队列
命令2 进入队列
命令3 进入队列
EXEC 执行事务
在 MULTI 之后、EXEC 之前,客户端发送的命令不会立刻执行,而是先进入事务队列。只有当 Redis 收到 EXEC 命令时,才会把队列里的命令按顺序执行。
对应图片:

执行机制
再说说它的执行机制,Redis 事务的基本流程是:
客户端发送 MULTI -> Redis 开启事务状态 -> 后续命令进入队列,返回 QUEUED -> 客户端发送 EXEC -> Redis 按顺序执行队列中的命令 -> 返回执行结果
执行过程可以理解为:
MULTI:告诉 Redis,我要开始打包命令了
SET name lin:先别执行,放进队列
INCR count:先别执行,放进队列
EXPIRE name 60:先别执行,放进队列
EXEC:现在开始按顺序执行队列里的命令
在事务执行期间,Redis 会连续执行这组命令,中间不会穿插其他客户端的命令。这个特性和 Redis 的单线程命令执行模型有关。但是要注意,Redis 事务不是 MySQL 那种完整强事务。它更像是:
命令队列 + 顺序提交。
对应图片:
常用命令
Redis 事务相关的核心命令主要有 5 个
| 命令 | 作用 | 理解 |
|---|---|---|
MULTI |
开启事务 | 后续命令不立即执行,而是进入队列 |
EXEC |
执行事务 | 按顺序执行队列里的所有命令 |
DISCARD |
取消事务 | 清空事务队列,不执行 |
WATCH |
监视 key | 用于实现乐观锁 |
UNWATCH |
取消监视 | 取消对 key 的监控 |
- MULTI:开启事务
表示开启一个事务。
执行 MULTI 后,客户端再发送的命令会进入事务队列,而不是立刻执行。
shell
MULTI
SET k1 v1
SET k2 v2
这时候 Redis 通常会返回:QUEUED,表示命令已经入队啦!没等到 EXEC 我是不会执行的!
- EXEC:提交并执行事务
表示提交事务。
执行 EXEC 后,Redis 会把事务队列中的命令按照进入队列的顺序依次执行。
shell
MULTI
SET name tom
INCR count
EXEC
真正修改数据的时间点不是 SET 和 INCR 发出时,而是 EXEC 发出之后。
- DISCARD:取消事务
表示取消当前事务,并清空事务队列。
例如:
MULTI
SET name tom
INCR count
DISCARD
执行 DISCARD 后,前面入队的 SET name tom 和 INCR count 都不会执行。同时要注意,如果开启事务后,服务器突然挂了,重新启动事务就不存在咯,也相当于discard的效果!
- WATCH:监视 key
用于监视一个或多个 key,常用来实现乐观锁。
基本思想是:
我先看一下这个 key 的值,然后准备修改它。如果在我提交事务之前,这个 key 被别人改过了,那我的事务就提交失败。
具体如何使用呢?在开启事务前watch一下key即可~
shell
WATCH stock
GET stock
MULTI
DECR stock
EXEC
如果在 WATCH stock 之后、EXEC 之前,有其他客户端修改了 stock,那么当前客户端的 EXEC 会失败。
- UNWATCH:取消监视
用于取消对 key 的监视。通常当你不想继续执行当前事务逻辑时,可以使用它取消监控。
与 MySQL 事务的区别
它们不是一个级别的东西,Redis 事务更轻量、更简单;MySQL 事务更完整、更严格。
| 对比项 | Redis 事务 | MySQL 事务 |
|---|---|---|
| 定位 | 命令打包 + 顺序执行 | 完整数据库事务 |
| 原子性 | 更像"整体提交,不被插队",只强调全部执行 | 强调真正的 all-or-nothing,确保全部都可以正确执行 |
| 回滚 | 不支持 MySQL 那样的自动回滚 | 支持回滚 |
| 隔离性 | 依赖 Redis 单线程顺序执行,只是事务执行期间不被插队 | 有完整隔离级别,如读已提交、可重复读等 |
| 一致性 | 通常不作为强一致事务理解 | ACID 语义更完整 |
| 适用场景 | 缓存、计数器、轻量批量更新 | 订单、支付、转账等强事务业务 |
对应图片:

原子性
这个地方很容易混乱,也是课堂板书里重点强调的地方。
单条 Redis 命令通常具有原子性
Redis 本身是单线程执行命令的,同一时刻只会处理一个命令。
所以像下面这种单条命令:
shell
INCR count
它本身通常可以理解为原子操作。不会出现加到一半被其他命令插进来的情况。
Redis 事务的"原子性"比较弱
Redis 事务的原子性,不能完全按照 MySQL 的事务原子性理解。
Redis 事务可以做到一组命令按顺序执行,执行过程中不会被其他客户端命令插队
但是它做不到 MySQL 那种要么全部成功、要么失败后全部回滚,它只确认命令能全部执行,但不保证完全对~
这就是 Redis 事务和 MySQL 事务非常大的区别。
MySQL 的原子性更严格
MySQL 事务强调的是要么全部成功、要么全部失败,而且支持失败后可以回滚的功能
Redis 并没有实现这种功能,虽然这种功能强大,但也是需要一定的性能开销,所以 Redis 没有实现这种功能开发者可能也有一定的考虑~
WATCH 实现思想:乐观锁
详细来说,Redis 的 watch 是基于版本号这样的机制实现了乐观锁
版本号这个概念在 CAS 中的 ABA 问题也涉及过,其思想方法和实现方式上是十分相似的~
虽然说我之前的博客也有对乐观锁、CAS 和 ABA 问题有详细的讲解,但不影响阅读体验,就在这简略介绍下

什么是乐观锁?
乐观锁的想法是:
我先假设冲突不会发生,所以不提前把资源锁死;等真正提交时,再检查有没有被别人改过。
-
悲观锁是:我觉得一定会有人抢,所以我先加锁。别人想操作,必须等我释放锁。
-
乐观锁是:我觉得大概率不会冲突,所以大家都可以先操作,提交时再检查有没有冲突,如果冲突了,我就失败重试。
版本号
当执行watch key时,这时就会给 key 分配一个版本号,此处的版本号可以理解为"一个整数",key 在每次修改后,版本号就会+1,此处假设 watch 之后 key 的版本号默认为1。
然后有两个客户端,客户端1执行了watch key,然后开启了事务,客户端2对key进行了更新操作,此时版本号+1。客户端1在队列中也安排了key的更新操作,然后客户端1 执行 exec,(exec 执行时间在客户端2对 key 更新操作之后)。此时 exec 在执行事务命令之后就会做出判定,判定watch时候的版本号与现在的版本号是否一致:如果一致,说明当前key在事务开启到最终执行的这个过程中都没有别的客户端进行修改,就能正常执行;反之,如果不一致,说明 key 在其他客户端就改过了,因此就丢弃了这个事务的操作,返回 nil ~

所以 watch 本质上不是给 key 上了一层字面意义上的 "锁",而是观察 key 是否被改过!
它能解决一部分问题,但不能自动解决所有问题。
MULTI/EXEC 能保证多条命令作为一组提交且按照顺序执行,执行过程中不被其他客户端命令插队
但是库存问题的关键通常不是简单地"几条命令一起执行",而是先判断库存是否足够再决定要不要扣减
而 Redis 事务有一个限制:
事务中的命令是先入队,EXEC 时才执行。客户端不能在事务执行过程中,根据上一条命令的结果临时决定下一条命令是否执行。
也就是说,下面这种思路在 Redis 事务里不够安全
shell
MULTI
GET stock
如果 stock > 0:
DECR stock
EXEC
因为 GET stock 在 EXEC 前只是进入队列,并没有真正返回可用于客户端判断的结果。
所以,单靠 MULTI/EXEC 不适合处理复杂的"先读结果,再根据结果决定是否写"的业务。
