Redis设计与实现之事务

一、事务

Redis 通过 MULTI 、DISCARD 、EXEC 和 WATCH 四个命令来实现事务功能,本章首先讨 论使用 MULTI 、DISCARD 和 EXEC 三个命令实现的一般事务,然后再来讨论带有 WATCH 的事务的实现。

因为事务的安全性也非常重要,所以本章最后通过常见的 ACID 性质对 Redis 事务的安全性进 行了说明。

1、事务

事务提供了一种“将多个命令打包,然后一次性、按顺序地执行”的机制,并且事务在执行的期 间不会主动中断——服务器在执行完事务中的所有命令之后,才会继续处理其他客户端的其他 命令。

以下是一个事务的例子,它先以 MULTI 开始一个事务,然后将多个命令入队到事务中,最后 由 EXEC 命令触发事务,一并执行事务中的所有命令:

redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"2) "C++"3) "Programming"

一个事务从开始到执行会经历以下三个阶段:

1. 开始事务。
2. 命令入队。
3. 执行事务。

下文将分别介绍事务的这三个阶段。

2、开始事务

MULTI 命令的执行标记着事务的开始:

redis> MULTI
OK

这个命令唯一做的就是,将客户端的 REDIS_MULTI 选项打开,让客户端从非事务状态切换到事 务状态。

3、命令入队

当客户端处于非事务状态下时,所有发送给服务器端的命令都会立即被服务器执行: 

redis> SET msg "hello moto"
OK
redis> GET msg
"hello moto"

但是,当客户端进入事务状态之后,服务器在收到来自客户端的命令时,不会立即执行命令, 而是将这些命令全部放进一个事务队列里,然后返回 QUEUED ,表示命令已入队:

redis> MULTI
OK
redis> SET msg "hello moto"
QUEUED
redis> GET msg
QUEUED

 以下流程图展示了这一行为:

事务队列是一个数组,每个数组项是都包含三个属性:

1. 要执行的命令(cmd)。
2. 命令的参数(argv)。
3. 参数的个数(argc)。

 举个例子,如果客户端执行以下命令:

redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED

 那么程序将为客户端创建以下事务队列:

4、执行事务

前面说到,当客户端进入事务状态之后,客户端发送的命令就会被放进事务队列里。

但其实并不是所有的命令都会被放进事务队列,其中的例外就是 EXEC 、DISCARD 、MULTI 和 WATCH 这四个命令——当这四个命令从客户端发送到服务器时,它们会像客户端处于非 事务状态一样,直接被服务器执行:

 如果客户端正处于事务状态,那么当 EXEC 命令执行时,服务器根据客户端所保存的事务队列,以先进先出(FIFO)的方式执行事务队列中的命令:最先入队的命令最先执行,而最后入 队的命令最后执行。

比如说,对于以下事务队列:

程序会首先执行 SET 命令,然后执行 GET 命令,再然后执行 SADD 命令,最后执行 SMEM- BERS 命令。

执行事务中的命令所得的结果会以 FIFO 的顺序保存到一个回复队列中。 比如说,对于上面给出的事务队列,程序将为队列中的命令创建如下回复队列:

当事务队列里的所有命令被执行完之后,EXEC 命令会将回复队列作为自己的执行结果返回给客户端,客户端从事务状态返回到非事务状态,至此,事务执行完毕。

事务的整个执行过程可以用以下伪代码表示:

def execute_transaction(): # 创建空白的回复队列reply_queue = []# 取出事务队列里的所有命令、参数和参数数量for cmd, argv, argc in client.transaction_queue: # 执行命令,并取得命令的返回值reply = execute_redis_command(cmd, argv, argc) # 将返回值追加到回复队列末尾reply_queue.append(reply) # 清除客户端的事务状态clear_transaction_state(client)# 清空事务队列 clear_transaction_queue(client)# 将事务的执行结果返回给客户端 send_reply_to_client(client, reply_queue)

5、在事务和非事务状态下执行命令

无论在事务状态下,还是在非事务状态下,Redis 命令都由同一个函数执行,所以它们共享很多服务器的一般设置,比如 AOF 的配置、RDB 的配置,以及内存限制,等等。 不过事务中的命令和普通命令在执行上还是有一点区别的,其中最重要的两点是:

  1. 非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不一定 是同一个;

    而事务状态则是以一个事务为单位,执行事务队列中的所有命令:除非当前事务执行完 毕,否则服务器不会中断事务,也不会执行其他客户端的其他命令。

  2. 在非事务状态下,执行命令所得的结果会立即被返回给客户端; 而事务则是将所有命令的结果集合到回复队列,再作为 EXEC 命令的结果返回给客户端。

6、事务状态下的 DISCARD 、MULTI 和 WATCH 命令

除了 EXEC 之外,服务器在客户端处于事务状态时,不加入到事务队列而直接执行的另外三

个命令是 DISCARD 、MULTI 和 WATCH 。

DISCARD 命令用于取消一个事务,它清空客户端的整个事务队列,然后将客户端从事务状态

调整回非事务状态,最后返回字符串 OK 给客户端,说明事务已被取消。

Redis 的事务是不可嵌套的,当客户端已经处于事务状态,而客户端又再向服务器发送 MULTI时,服务器只是简单地向客户端发送一个错误,然后继续等待其他命令的入队。MULTI 命令 的发送不会造成整个事务失败,也不会修改事务队列中已有的数据。

WATCH 只能在客户端进入事务状态之前执行,在事务状态下发送 WATCH 命令会引发一个 错误,但它不会造成整个事务失败,也不会修改事务队列中已有的数据(和前面处理 MULTI 的情况一样)。

7、带 WATCH 的事务

WATCH 命令用于在事务开始之前监视任意数量的键:当调用 EXEC 命令执行事务时,如果任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行,直接返回失败。 以下示例展示了一个执行失败的事务例子:

redis> WATCH name
OK
redis> MULTI
OK
redis> SET name peter
QUEUED
redis> EXEC
(nil)

 以下执行序列展示了上面的例子是如何失败的:

在时间 T4 ,客户端 B 修改了 name 键的值,当客户端 A 在 T5 执行 EXEC 时,Redis 会发现name 这个被监视的键已经被修改,因此客户端 A 的事务不会被执行,而是直接返回失败。

下文就来介绍 WATCH 的实现机制,并且看看事务系统是如何检查某个被监视的键是否被修 改,从而保证事务的安全性的。

8、WATCH 命令的实现

在每个代表数据库的 redis.h/redisDb 结构类型中,都保存了一个 watched_keys 字典,字典 的键是这个数据库被监视的键,而字典的值则是一个链表,链表中保存了所有监视这个键的客 户端。

比如说,以下字典就展示了一个 watched_keys 字典的例子:

其中,键 key1 正在被 client2 、client5 和 client1 三个客户端监视,其他一些键也分别被 其他别的客户端监视着。

WATCH 命令的作用,就是将当前客户端和要监视的键在watched_keys中进行关联。 举个例子,如果当前客户端为 client10086 ,那么当客户端执行 WATCH key1 key2 时,前面展示的 watched_keys 将被修改成这个样子:

通过 watched_keys 字典,如果程序想检查某个键是否被监视,那么它只要检查字典中是否存 在这个键即可;如果程序要获取监视某个键的所有客户端,那么只要取出键的值(一个链表), 然后对链表进行遍历即可。

9、WATCH 的触发

在任何对数据库键空间(key space)进行修改的命令成功执行之后(比如 FLUSHDB 、SET 、DEL 、LPUSH 、SADD 、ZREM ,诸如此类),multi.c/touchWatchKey 函数都会被调用 ——它检查数据库的 watched_keys 字典,看是否有客户端在监视已经被命令修改的键,如果 有的话,程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开:

 

当客户端发送 EXEC 命令、触发事务执行时,服务器会对客户端的状态进行检查:

  • 如果客户端的REDIS_DIRTY_CAS选项已经被打开,那么说明被客户端监视的键至少有一 个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端 返回空回复,表示事务执行失败。

  • 如果REDIS_DIRTY_CAS选项没有被打开,那么说明所有监视键都安全,服务器正式执行 事务。

 可以用一段伪代码来表示这个检查:

def check_safety_before_execute_trasaction():if client.state && REDIS_DIRTY_CAS: # 安全性已破坏,清除事务状态 clear_transaction_state(client) # 清空事务队列 clear_transaction_queue(client) # 返回空回复给客户端 send_empty_reply(client)else# 安全性完好,执行事务 execute_transaction()

举个例子,假设数据库的 watched_keys 字典如下图所示:

如果某个客户端对 key1 进行了修改(比如执行 DEL key1 ),那么所有监视 key1 的客户端,包 括 client2 、client5 和 client1 的 REDIS_DIRTY_CAS 选项都会被打开,当客户端 client2 、client5 和 client1 执行 EXEC 的时候,它们的事务都会以失败告终。

最后,当一个客户端结束它的事务时,无论事务是成功执行,还是失败,watched_keys 字典中和这个客户端相关的资料都会被清除。

10、 事务的 ACID 性质

在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的安全性。
Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。 以下四小节是关于这四个性质的详细讨论。

原子性(Atomicity)

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以Redis 事务的执行并不是原子性的。 如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功。

另一方面,如果 Redis 服务器进程在执行事务的过程中被停止——比如接到 KILL 信号、宿主 机器停机,等等,那么事务执行失败。

当事务失败时,Redis 也不会进行任何的重试或者回滚动作。

一致性(Consistency)

Redis 的一致性问题可以分为三部分来讨论:入队错误、执行错误、Redis 进程被终结。 入队错误

在命令入队的过程中,如果客户端向服务器发送了错误的命令,比如命令的参数数量 不对,等等,那么服务器将向客户端返回一个出错信息,且将客户端的事务状态设REDIS_DIRTY_EXEC。

当客户端执行 EXEC 命令时,Redis 会拒绝执行状态为 REDIS_DIRTY_EXEC 的事务,并返回失 败信息。

redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> set key
(error) ERR wrong number of arguments for 'set' command
redis 127.0.0.1:6379> EXISTS key
QUEUED
redis 127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

因此,带有不正确入队命令的事务不会被执行,也不会影响数据库的一致性。

执行错误

如果命令在事务执行的过程中发生错误,比如说,对一个不同类型的 key 执行了错误的操作, 那么 Redis 只会将错误包含在事务的结果中,这不会引起事务中断或整个失败,不会影响已执 行事务命令的结果,也不会影响后面要执行的事务命令,所以它对事务的一致性也没有影响。

Redis 进程被终结

如果 Redis 服务器进程在执行事务的过程中被其他进程终结,或者被管理员强制杀死,那么根

据 Redis 所使用的持久化模式,可能有以下情况出现:

  • 内存模式:如果 Redis 没有采取任何持久化机制,那么重启之后的数据库总是空白的,所

    以数据总是一致的。

  • RDB 模式:在执行事务时,Redis 不会中断事务去执行保存 RDB 的工作,只有在事务执 行之后,保存 RDB 的工作才有可能开始。所以当 RDB 模式下的 Redis 服务器进程在事 务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到 RDB 文件里。 恢复数据库需要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数 据库快照(snapshot),所以它的数据可能不是最新的,但只要 RDB 文件本身没有因为 其他问题而出错,那么还原后的数据库就是一致的。

  • AOF 模式:因为保存 AOF 文件的工作在后台线程进行,所以即使是在事务执行的中途, 保存 AOF 文件的工作也可以继续进行,因此,根据事务语句是否被写入并保存到 AOF 文件,有以下两种情况发生:

    1)如果事务语句未写入到 AOF 文件,或 AOF 未被 SYNC 调用保存到磁盘,那么当进 程被杀死之后,Redis 可以根据最近一次成功保存到磁盘的 AOF 文件来还原数据库,只 要 AOF 文件本身没有因为其他问题而出错,那么还原后的数据库总是一致的,但其中的 数据不一定是最新的。

    2)如果事务的部分语句被写入到 AOF 文件,并且 AOF 文件被成功保存,那么不完整的 事务执行信息就会遗留在 AOF 文件里,当重启 Redis 时,程序会检测到 AOF 文件并不 完整,Redis 会退出,并报告错误。需要使用 redis-check-aof 工具将部分成功的事务命令 移除之后,才能再次启动服务器。还原之后的数据总是一致的,而且数据也是最新的(直 到事务执行之前为止)。

    隔离性(Isolation)

    • 在单纯的内存模式下,事务肯定是不持久的。

    • 在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所

      以 RDB 模式下的 Redis 事务也是不持久的。

    • 在 AOF 的“总是 SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用 fsync

      或 fdatasync 将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主

      线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段

      非常小的间隔,所以这种模式下的事务也是不持久的。
      其他 AOF 模式也和“总是 SYNC ”模式类似,所以它们都是不持久的。

  • Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执

    行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。 持久性(Durability)

    因为事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事 务的持久性由 Redis 所使用的持久化模式决定:

二、 小结

  • 事务提供了一种将多个命令打包,然后一次性、有序地执行的机制。

  • 事务在执行过程中不会被中断,所有事务命令执行完之后,事务才能结束。

  • 多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行。

  • 带WATCH命令的事务会将客户端和被监视的键在数据库的watched_keys字典中进行关 联,当键被修改时,程序会将所有监视被修改键的客户端的 REDIS_DIRTY_CAS 选项打开。

  • 只有在客户端的REDIS_DIRTY_CAS选项未被打开时,才能执行事务,否则事务直接返回 失败。

  • Redis 的事务保证了 ACID 中的一致性(C)和隔离性(I),但并不保证原子性(A)和 持久性(D)。

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/229725.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

18个非技术面试题

请你自我介绍一下你自己? 这道面试题是大家在以后面试过程中会常被问到的,那么我们被问到之后,该如果回答呢?是说姓名?年龄?还是其他什么? 最佳回答提示: 一般人回答这个问题往往会…

neuq-acm预备队训练week 9 P1119 灾后重建

解题思路 本题可以用最短路算法——Floyd AC代码 #include<bits/stdc.h> #define inf 1e9 using namespace std; const int N 2e2 50; int n, m, q, now 0, a, b, c, t[N], G[N][N];int main() {scanf("%d%d", &n, &m);for(int i 0;i<n;i)sc…

2023新时代中国模特大赛总决赛在京落幕

12月16日&#xff0c;备受瞩目的2023新时代中国模特大赛圆满落幕。本次大赛旨在挖掘和培养具有新时代特色的模特人才&#xff0c;推动中国时尚产业的创新发展。 作为中国时尚界的重要赛事&#xff0c;新时代中国模特大赛吸引了来自全国各地的优秀模特选手45名参加全国总决赛。在…

The Grid – Responsive WordPress Grid响应式网格插件

点击阅读The Grid – Responsive WordPress Grid响应式网格插件原文 The Grid – Responsive WordPress Grid响应式网格插件是一个高级 wordpress 网格插件&#xff0c;它允许您在完全可定制且响应迅速的网格系统中展示任何自定义帖子类型。 Grid WordPress 非常适合展示您的博…

QT信号、槽机制介绍与实现

时间记录&#xff1a;2023/12/17 1.介绍 QT中进行对象之间事件的处理机制为信号、槽机制&#xff0c;即一个对象状态改变时发出信号&#xff0c;然后与此信号进行绑定的对应槽便会被触发&#xff0c;类似于c/c里面的回调函数机制 2.信号 在一定情况下被发射的事件&…

【TB作品】51单片机,语音出租车计价器

西交大题目 1.语音出租车计价器 一、功能要求: 1.具有可模拟出租车车轮转速传感器的硬件设计,可计量出租车所走的公 里数。 2.显示和语音播报里程、价格和等待红灯或堵车的计时价格: 3.具有等待计时功能 4.具有实时年月日显示和切换功能。 5.操作简单、界面友好。 二、设计建议…

08-工厂方法

意图 定义一个用于创建对象的接口&#xff0c;让子类决定实例化哪一个类 类图 适用性 在下列情况可以使用工厂方法模式&#xff1a; 当一个类不知道它所必须创建的对象的类的时候。当一个类希望由它的子类来指定它所创建的对象的时候。当类将创建对象的职责委托给多个帮助子…

ELK(八)—Metricbeat部署

目录 介绍修改配置文件启动 Modulenginx开启状态查询配置Nginx module查看是否配置成功 介绍 Metricbeat 是一个轻量级的开源度量数据收集器&#xff0c;用于监控系统和服务。它由 Elastic 公司开发&#xff0c;并作为 Elastic Stack&#xff08;Elasticsearch、Logstash、Kiba…

【Linux】在vim中批量注释与批量取消注释

在vim编辑器中&#xff0c;批量注释和取消注释的操作可以通过进入V-BLOCK模式、选择要注释或取消注释的内容、输入注释符号或选中已有的注释符号和按键完成。这些操作可以大大提高代码或文本的编写和修改效率&#xff0c;是vim编辑器中常用的操作之一。 1.在vim中批量注释的步…

[elementPlus] teleported 在 ElSubMenu中的用途

如图 一个菜单对应的路由结构如上图 如果做适配窄屏幕 如果在 <ElSubMenu :index"route.path" >中不加入 teleported 就会出现问题 加上就OK了 <ElSubMenu :index"route.path" teleported>

力扣200. 岛屿数量(java DFS解法)

Problem: 200. 岛屿数量 文章目录 题目描述思路解题方法复杂度Code 题目描述 思路 该问题可以归纳为一类遍历二维矩阵的题目&#xff0c;此类中的一部分题目可以利用DFS来解决&#xff0c;具体到本题目&#xff1a; 1.我们首先要针对于二维数组上的每一个点&#xff0c;尝试展…

单片机期末复习

前言 发现很多人都写了单片机原理及接口技术课后习题的答案&#xff0c;但是也就只写了答案而已&#xff0c;可能是他们觉得太简单的缘故吧&#xff0c;我这里对此进行一下我近段时间复习的总结&#xff0c;本篇博客只展示选择题、填空题和判断题的答案&#xff0c;仅供参考&a…

饥荒Mod 开发(十一):修改物品堆叠

饥荒Mod 开发(十)&#xff1a;制作一把AOE武器 饥荒Mod 开发(十二)&#xff1a;一键制作 饥荒中物品栏有限&#xff0c;要拾取的物品有很多&#xff0c;经常装不下要忍痛丢掉各种东西&#xff0c;即使可以将物品放在仓库但是使用不方便&#xff0c;所以可以将物品的堆叠个数设…

1264. 动态求连续区间和(树状数组---某个位置加上一个数/求在线(动态)前缀和/蓝桥杯)

题目&#xff1a; 输入样例&#xff1a; 10 5 1 2 3 4 5 6 7 8 9 10 1 1 5 0 1 3 0 4 8 1 7 5 0 4 8输出样例&#xff1a; 11 30 35 树状数组&#xff1a; 代码&#xff1a; #include<cstdio> #include<iostream> using namespace std;const int N100010; int n,…

系统架构设计师教程(七)系统架构设计基础知识

系统架构设计基础知识 7.1 软件架构概念7.1.1 软件架构的定义7.1.2 软件架构设计与生命周期需求分析阶段设计阶段实现阶段构件组装阶段部署阶段后开发阶段 7.1.3 软件架构的重要性 7.2 基于架构的软件开发方法7.2.1 体系结构的设计方法概述7.2.2 概念与术语7.2.3 基于体系结构的…

深度剖析JavaScript中冒泡和捕获机制、事件代理

JS事件传播的两种机制包括冒泡和捕获&#xff0c;下面将具体剖析它们之间本质的区别。 事件冒泡: 先触发子元素的事件&#xff0c;再触发父元素的事件。 创建一个 ul label 和 li label, 分别绑定一个父id 和 子 id, 再通过创建 script&#xff0c;去绑定各自的点击事件。 <…

阿里云|人工智能(AI)技术解决方案

函数计算部署Stable Diffusion AI绘画技术解决方案 通过函数计算快速部署Stable Diffusion模型为用户提供快速通过文字生成图片的能力。该方案通过函数计算快速搭建了AIGC的能力&#xff0c;无需管理服务器等基础设施&#xff0c;专注模型的能力即可。该方案具有高效免运维、弹…

设计模式——责任链模式(行为模式)

引言 责任链模式是一种行为设计模式&#xff0c; 允许你将请求沿着处理者链进行发送。 收到请求后&#xff0c; 每个处理者均可对请求进行处理&#xff0c; 或将其传递给链上的下个处理者。 问题 假如你正在开发一个在线订购系统。 你希望对系统访问进行限制&#xff0c; 只允…

Pod控制器详解【五】

文章目录 5. Pod控制器详解5.1 Pod控制器介绍5.2 ReplicaSet(RS)5.3 Deployment(Deploy)5.4 Horizontal Pod Autoscaler(HPA)5.5 DaemonSet(DS)5.6 Job5.7 CronJob(CJ) 5. Pod控制器详解 5.1 Pod控制器介绍 Pod是kubernetes的最小管理单元&#xff0c;在kubernetes中&#xf…

docker文档转译1

写在最前面 本文主要是转译docker官方文档。主题是Docker overview&#xff0c;这里是链接 Docker概述 Docker是一个用于开发、发布和运行应用程序的开放平台。Docker使你能够将应用程序与基础设施分离&#xff0c;从而可以快速交付软件。你可以使用相同的方法像管理应用程序…