目录
[Redis 的事务](#Redis 的事务)
Redis 的事务
在 MySQL事务-CSDN博客 中,我们学习了 MySQL 的事务,MySQL 的事务是一组操作的集合 ,这组操作要么全部成功,要么全部失败 ,以确保数据的一致性 和完整性
且 MySQL 的事务具有四个核心特性:
**(1)原子性 (Atomicity):**通过事务,将多个操作打包为一个整体
**(2)一致性 (Consistency):**在事务执行前后,数据库的状态必须保持一致,防止出现上述数据库中间出现问题
**(3)隔离性 (Isolation):**多个事务在并发执行时,可能会存在一定的问题,因此,我们就需要具体情况具体分析,判断需要当前数据尽量准确,还是希望执行速度尽可能的快
(4)持久性 (Durability):事务的任何修改,都是持久化存在的(写入硬盘的),无论是重启程序,还是重启主机,修改的数据都是不会丢失的
我们就通过对比 MySQL 的事务,来学习 Redis 的事务,首先来看原子性:
Redis 的事务是将一些列操作绑定为一组 ,让这组操作能够批量执行
对比 MySQL 的事务,MySQL 会保证事务要么全部成功,要么全部失败 ,而 Redis 的事务只能做到让这些操作批量执行 ,若执行过程中出现问题,并不支持回滚,也就是说,若其中的一个命令执行失败了,其他命令仍然会执行
接下来看 一致性:
MySQL 通过约束、回滚等机制来确保事务执行前后数据始终保持业务逻辑和完整性规则;
而 Redis 不涉及约束,也不支持回滚,因此,Redis 不保证一致性
然后是 隔离性:
MySQL 通过提供多种隔离级别 (读已提交、可重复读等),以精细化控制并发事务间的相互影响
但是,Redis 是单线程处理请求 的,因此,不涉及并发执行事务 ,也就不需要隔离性
最后来看持久性:
MySQL 通过事务日志,确保事务一旦提交,其修改永久保存,即使系统故障也能恢复;
而 Redis 是一个内存数据库 ,它将数据存储在内存 中,其持久化依赖RDB/AOF 配置( Redis 持久化-CSDN博客) ,与事务无关,若未开启持久化,或事务完成后系统立即崩溃,都会导致数据丢失,因此 Redis 不保证持久化
我们来总结一下 :
**1. Redis 部分支持原子性:**Redis 只能保证命令打包执行,但不支持回滚,若其中有命令执行失败,其他命令仍会执行
2. Redis 不保证一致性:Redis 不涉及约束,也不支持回滚,无法保证一致性
**3. Redis 不需要隔离性:**Redis 是单线程处理请求 ,不涉及并发执行事务,也就不需要隔离性
4. Redis 不保证持久化: Redis 是一个内存数据库,将数据存储在内存中,其持久化依赖RDB/AOF 配置,与事务无关
此外,Redis 事务本质上是通过一个**"事务队列"** 来实现的,当用户开启事务,就会将后续命令都先存放在这个事务队列中,而不是立即执行事务 ,等到服务器收到执行事务 命令时,才会按出队顺序 依次执行队列中的每条命令,在执行过程中,不会处理任何其他客户端的请求(即 单线程天然隔离),执行结果依次存入一个数组返回给客户端
接下来,我们就来学习 Redis 的事务操作
事务操作
MULTI:开启事务,执行成功返回 OK
EXEC:执行事务
DISCARD:放弃当前事务,此时会直接清空事务队列,之前的操作都不会真正执行
执行事务
我们使用 客户端1开启事务并添加命令:

可以看到,开启事务后,每添加一个操作,都会返回 "QUEUED ",说明命令已经放入事务队列中,若我们此时通过 客户端2获取设置的 key1:

此时 key1 并不存在,客户端1 执行 EXEC命令:

此时,才会真正执行命令,并返回所有命令执行结果,此时我们再使用 客户端2获取 key1:

此时就可以获取到 key1 了
我们继续看放弃事务
放弃事务

放弃事务后,之前的操作都不会执行,此时获取 key4,key4 并未被设置,也就获取失败
WATCH
在执行事务时,若事务中修改的某个值,被其他客户端修改了,此时就可能出现数据不一致的问题
例如:

客户端1 开启事务,并设置 key 为111:

客户端2 设置 key 为222:

客户端1 执行事务:

查看 key 最终的值:

可以看到,最终 key 的值为 111
这是因为虽然 客户端1先设置key,但此时命令被存放到队列中,并未真正执行;
而 客户端2未开启事务,设置key命令立即执行;
当 客户端1 执行 EXEC 命令,set key 命令真正执行,覆盖了 客户端2 的设置
这种情况,就导致了客户端2 设置的值被覆盖,就可能会出现问题,因此 Redis 提供了 WATCH命令来解决这个问题
WATCH 能够监视一组 key ,若被监视的 key 在WATCH 执行之后、EXEC 执行之前被其他客户端修改 ,就会让整个事务执行失败 ,EXEC 命令返回 (nil)
Redis 服务器会为当前客户端 WATCH 监视的键打上标记
若其他客户端对被监视 key 进行修改(如 set、del、incr、lpush等),Redis会检查 key 是否被某个客户端 WATCH,若是,则会将该客户端的 REDIS_DIRTY_CAS标志设为 "脏"
当客户端执行 EXEC 时,Redis 会检查客户端的 REDIS_DIRTY_CAS标记:
若标记为脏 -> 放弃执行事务,清空事务队列,返回 (nil)
若标记干净-> 正常执行事务队列中所有命令
例如:
我们在 客户端1 监视 k1 和 k2,并开启事务:

此时,在 客户端2修改 k2:

再在 客户端1中执行 EXEC:

可以看到,只要被 WATCH 监视的 key,在 WATCH 执行之后,EXEC 执行之前,被当前客户端以外的其他客户端修改,当前客户端的事务就会执行失败
即使当前客户端的事务队列中完全没有操作那些被监视的 key(如上述只修改了k1,未修改k2),只要外部有别的客户端改动过这些被监视的 key,事务都会失败
此外,通过 UNWATCH 命令可以取消对 key 的监视 。实际上,执行 EXEC 或 DISCARD 后,当前连接的所有监视都会被自动清空,相当于隐式调用 UNWATCH;也可以手动调用 UNWATCH 来取消监视
而上述这种检查-设置 模式,正是 乐观锁实现的核心
乐观锁: 假设并发冲突很少发生 ,所以不加锁 ,只是在更新时检查数据在此期间是否被修改。如果被修改则放弃或重试
例如,通过WATCH + 事务 实现减扣库存,并防止超卖:
Lua
# 客户端 A
WATCH stock:item_123 # 1. 监视库存键
val = GET stock:item_123 # 2. 读取当前库存,假设为 10
if val > 0:
MULTI # 3. 开启事务
DECR stock:item_123 # 4. 扣减库存
result = EXEC # 5. 执行事务
if result is None:
# 事务失败,库存被其他客户端修改了,重试或放弃
else:
# 事务成功,扣减成功
else:
UNWATCH # 库存不足,取消监视
客户端A 监控库存键,在开启事务之前先判断当前库存是否充足,若充足,则开启事务,并减扣库存
若在事务开启期间,其他客户端修改了库存 stock:item_123 ,将其从 10 减为 9
客户端A在执行事务时发现 stock:item_123 被修改,执行失败,从而避免基于过时数据进行的操作
事务队列
在 Redis 事务中,服务器为每个处于 MULTI 状态的客户端 临时维护了一个先进先出(FIFO)的命令缓冲区 ,也就是事务队列:
当客户端发送 MULTI 时,服务器就会开启该客户端的事务模式,并创建一个空的命令队列
之后发送的普通命令 (如 set、incr)都不会立即被执行,而是将命令名称、参数、参数个数 等完整信息按照顺序追加到队列尾部,并返回 QUEUED
收到 EXEC 时,服务器按照出队顺序依次执行队列中的每条命令,期间不会处理其他客户端的请求
无论是 EXEC 还是 DISCARD,队列最终都会被清空,客户端退出事务状态
事务相关命令(EXEC、DISCARD、WATCH、MULTI)不会入队:
EXEC:触发队列执行
DISCARD:清空队列并退出事务模式
WATCH:必须在 MULTI 之前,不属于队列
MULTI:开启事务,且事务中不能再嵌套 MULTI