欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 创投人物 > [Redis#16] 事务 | vs Mysql | 命令 | WATCH的实现

[Redis#16] 事务 | vs Mysql | 命令 | WATCH的实现

2025/2/24 22:32:52 来源:https://blog.csdn.net/2301_80171004/article/details/144275550  浏览:    关键词:[Redis#16] 事务 | vs Mysql | 命令 | WATCH的实现

目录

什么是事务

实现事务的方式

Redis 事务与 MySQL 事务的对比

应用场景:防止超卖

Lua 脚本增强

事务操作

MULTI & EXEC

DISCARD

WATCH

WATCH 的实现原理


什么是事务

[MySQL#12] 事务(1) | ACID | commit | 回滚 | 常见操作

Redis 的事务和 MySQL 的事务概念上是类似的,都是把一系列操作绑定成一组,让这一组能够批量执行。然而,需要注意的是 Redis 事务与 MySQL 事务存在以下几点区别:

  • 弱化的原子性: Redis 没有 "回滚机制"。它只能保证这些操作 "批量执行",但不能做到 "一个失败就恢复到初始状态"。
  • 不保证一致性: Redis 不涉及 "约束",也没有回滚机制。MySQL 的一致性体现在运行事务前后结果的合理性,不会出现中间非法状态。
  • 不需要隔离性: Redis 事务没有隔离级别,因为 Redis 是单线程处理请求,不会并发执行事务。
  • 不需要持久性: Redis 数据保存在内存中,是否开启持久化由 redis-server 自行决定,这与事务无关。

Redis 事务本质上是在服务器上创建了一个 "事务队列"。

每次客户端在事务中进行一个操作,都会先将命令发送给服务器并放入 "事务队列"(但并不会立即执行),而是在收到 EXEC 命令后,才真正执行队列中的所有操作。

  • 这么多特性Redis都不具备,那么Redis的事务到底有什么意义呢?
    Redis的事务最主要的意义就是为了"打包",避免其他客户端的命令插队到中间.

最开始官网还说原子性,后来就把这句话给删了,官方也是有点虚的


实现事务的方式

  • Redis 在实现事务时引入了队列机制。每个客户端都有一个独立的队列。
  • 当开启事务时,客户端输入的命令会发送给服务器并进入这个队列中,而不是立即执行。
  • 只有当遇到 "执行事务" 的命令(如 EXEC)时,才会把队列中的任务按照录入顺序依次执行。
  • 这些事务操作是在 Redis 主线程中完成的。
  • 主线程会确保将事务中的所有操作执行完毕后,再处理其他客户端的请求。

Redis 事务与 MySQL 事务的对比

虽然 Redis 事务的功能没有 MySQL 强大,但 MySQL 为了实现强大的事务功能也付出了不小的代价:

  • 空间上:MySQL 需要花费更多空间来存储额外的数据,例如用于回滚段和多版本控制。
  • 时间上:MySQL 的事务处理需要更大的执行开销,包括锁管理、日志记录等。

正因为 MySQL 事务存在上述问题,Redis 提供了一种轻量级的解决方案,适用于某些特定场景。

应用场景:防止超卖

在购物网站秒杀场景中,可能会出现超卖的情况,即商家放了5000个商品,但由于并发问题导致下单成功了5001个。为避免这种情况,在多线程环境中我们曾通过加锁的方式来解决。

如果引入 Redis,则可以直接使用其事务机制解决问题。

例如,考虑两个客户端几乎同时下单的情况:

  • 客户端1开启一个事务,该事务被放入 Redis 的事务队列中;
  • 客户端2同样开启一个事务,并被放入队列;
  • 当事务1收到执行命令被执行时,它会减少库存计数(count--);
  • 当轮到事务2执行时,由于此时 count>0 的条件不再满足,所以事务2将不会改变数据库状态,从而避免了超卖问题。

Lua 脚本增强

值得注意的是,尽管 Redis 本身不支持像编程语言那样的条件判断(如 if),但它可以引入 Lua 脚本来实现复杂的逻辑判断。通过 Lua 脚本,我们可以实现上述条件的判断逻辑,进一步强化 Redis 事务的应用能力。


事务操作

MULTI & EXEC
  • multi:开启事务
  • exec:提交事务

示例:

开启事务后,不论输入什么指令,都返回QUEUE,此时会在客户端维护一个队列,将所有指令入队列。最后执行exec提交事务,所有的指令再同时返回。

DISCARD
  • discard:取消事务

示例:

开启事务后,输入两个值,再通过discard取消事务,最后查询key1发现插入失败,因为这个请求没有提交给客户端,而是直接被取消了。

另外的,如果在事务执行的过程中,Redis崩溃,恢复后效果和discard一样,就是事务内的操作都取消了。

WATCH

现有以下场景:

  • 从时间上来看,客户端1 是先发送了 set key 222,客户端2 是后发送了 set key 333
  • 由于 客户端1 中,得是 exec 执行了,才会真正执行 set key 222 这个操作
  • set key 222 变成了实际上更晚执行的操作!! 最终值就是 222

在事务执行过程中,客户端1无法感知外部的变化,客户端2的命令被 客户端 1 事务覆盖了

  • watch:让事务可以监听外部的变化,如果监听的数据被外部改变,操作失效

语法:

左侧的终端,在执行事务前开启了watch key,开启事务后set key 222。在事务提交前,右侧终端修改了key 333,随后左侧终端提交事务时,返回了nil表示事务操作无效。

因为watch监听到了key被外部修改,此时自己的事务提交可能会影响其它客户端,于是取消该操作。

如果想要取消watch,可以使用unwatch指令:

unwatch

这个指令没有参数,一次性取消所有key的监听。

WATCH 的实现原理

WATCH 的实现原理类似于我们在并发编程中学习的乐观锁机制,即解决 CAS(Compare-And-Swap)中的 ABA 问题的策略。乐观锁假设冲突很少发生,并在检测到冲突时采取相应的措施。与 ABA 问题中的实现策略相似,Redis 的 WATCH 基于版本号机制实现了乐观锁。

watch使用了一种版本号的机制每个数据都有一个版本号,每次修改key的值时,都会修改其版本号。

  • 在watch key时,会记录当前的版本号。
  • 在事务提交时,检测当前的版本号是否与之前的版本号相同,如果相同那么提交成功,如果版本号不同,说明有别的用户修改了数据,导致版本号修改,当前事务将不会执行并返回失败。

WATCH 本质上是给 EXEC 加上了一个对特定 key 的判定条件只有当所有被 WATCH 的 key 自从 WATCH 开始以来没有被修改过的情况下,事务才会被执行;否则,事务将被取消。

举例说明

可以将 WATCH 比喻为判断老公是否出轨的情景:

  • 离开家之前:把枕头放到一个特定的位置,并拍下一张照片作为记录。
  • 回家之后:检查枕头的位置是否发生了改变。如果位置变了,则可以推测出在这段时间内有人移动了枕头(在这个比喻中意味着老公可能出轨了)。

同样的逻辑应用于 Redis 中:

  • WATCH key3:开始监视 key,此时记录下 key 的状态(版本号)。
  • 尝试执行事务:如果在此期间 key 被其他客户端修改过,那么提交事务时就会发现 key 的状态已经改变,从而导致当前事务执行失败。如下就返回了 Nil

乐观锁与悲观锁

  • 乐观锁:乐观锁假设在加锁之前,锁冲突的概率较低。因此,在加锁之前不会进行任何检查,而是直接进行操作。如果在操作完成后发现冲突,则会进行重试。
  • 悲观锁:悲观锁假设在加锁之前,锁冲突的概率较高。因此,在加锁之前会进行检查,确保没有其他线程正在操作该资源。如果发现冲突,则会阻塞等待。

实现示例

  • 在 C++ 和 Linux 中涉及的锁,如 mutex 和 std::mutex,都是悲观锁。
  • Java 中的 synchronized 关键字则可以在悲观/乐观之间自适应。

事务命令 sum:

  • MULTI:开启一个事务,执行成功返回 OK。
  • EXEC:真正执行事务。每个操作加入事务时会提示 "QUEUED",表示命令已经进入客户端的队列,直到 EXEC 执行时才会真正发送给服务器。
  • DISCARD:放弃当前事务,清空事务队列,之前的操作都不会真正执行。
  • WATCH:用于监控一组具体的 key,在提交事务时如果发现 key 被其他客户端修改过,则事务执行失败。
  • UNWATCH:取消对 key 的监控,相当于 WATCH 的逆操作。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词