当前位置: 首页 > article >正文

[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 的逆操作。

http://www.kler.cn/a/430309.html

相关文章:

  • 记录一次面试中被问到的问题 (HR面)
  • SpringBoot环境和Maven配置
  • 实习总结(项目篇)
  • 数字IC设计高频面试题
  • Linux(Centos 7.6)命令详解:mkdir
  • 【动态重建】时间高斯分层的长体积视频
  • 云原生概念讲解一下
  • 如何在 JavaScript 中进行深度克隆?
  • C语言 while/do-while/for/goto
  • 组件开发的环境准备: nodejs安装,npm镜像源的修改,pnpm包管理器的安装(全局安装),基于pnpm创建脚手架项目
  • 性能测试攻略(一):需求分析
  • 【Linux】通过crond服务设置定时执行shell脚本,实际执行时间却延迟了8小时
  • NASH均衡存在性证明
  • Python 3 和 MongoDB 的集成使用
  • C#实现一个HttpClient集成通义千问-多轮对话功能实现
  • Bluetooth LE AUDIO架构概述
  • /usr/local/go/bin/go: cannot execute binary file: Exec format error
  • go基础总结
  • 蓝桥杯刷题日记01-握手问题
  • C++ 基础教学:开启编程新征程
  • ubuntu系统每天凌晨定时上传redis 备份数据到阿里云OSS上
  • 火语言RPA流程组件介绍--鼠标点击
  • 从0开始深度学习(35)——YOLO V5原理详解
  • Python 网络爬虫进阶2:突破数据采集的边界
  • Spring Boot 整合 Druid 并开启监控
  • 16 设计模式之适配器模式(充电器转换案例)