【Redis】事务详解、WATCH 实现思想

事务

核心意义

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 不适合处理复杂的"先读结果,再根据结果决定是否写"的业务。

相关推荐
SimonKing1 小时前
你还在靠重启来调线程池?别人已经做到了实时调控,3分钟接入
java·后端·程序员
小张小张爱学习1 小时前
Java并发编程面试题
java·开发语言
码不停蹄的玄黓1 小时前
JDK 自带四大命令行工具:jstat、jstack、jmap、jhat 详解
java·开发语言
霸道流氓气质1 小时前
异步任务提交 + Redis 状态轮询模式实战指南
数据库·redis·缓存
ch.ju1 小时前
Java程序设计(第3版)第四章——set方法为属性赋值
java·开发语言
霸道流氓气质2 小时前
Spring Boot + Jasypt 实战指南:配置文件敏感信息加密完全手册
数据库·spring boot·oracle
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第94题】【Mysql篇】第24题:什么是单路排序?什么是双路排序??
java·开发语言·数据库·mysql·面试·排序算法
我是一颗柠檬2 小时前
【Java项目技术亮点】多级缓存一致性方案:Canal+MQ实现数据库与缓存的最终一致
java·数据库·spring·缓存·kafka·rocketmq
于先生吖2 小时前
Java分账体系设计,网约车行程计费与到店线下结账一体化后端开发实战
java·开发语言