三次给你聊清楚Redis》之彻底把单机聊透

网友投稿 500 2022-05-29

3.5、事务

Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

批量操作在发送 EXEC 命令前被放入队列缓存。

收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。

在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

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

开始事务。

命令入队。

执行事务。

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

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

详细介绍:

3.5.1事务开始

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

redis> MULTI OK

MULTI 命令可以将执行该命令的客户端从非事务状态切换至事务状态, 这一切换是通过在客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成的, MULTI 命令的实现可以用以下伪代码来表示:

def MULTI(): # 打开事务标识 client.flags |= REDIS_MULTI # 返回 OK 回复 replyOK()

3.5.2命令入队

当一个客户端处于非事务状态时, 这个客户端发送的命令会立即被服务器执行:

redis> SET "name" "Practical Common Lisp" OK redis> GET "name" "Practical Common Lisp" redis> SET "author" "Peter Seibel" OK redis> GET "author" "Peter Seibel"

与此不同的是, 当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:

如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。

与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。

3.5.3事务队列

每个 Redis 客户端都有自己的事务状态, 这个事务状态保存在客户端状态的 mstate 属性里面:

typedef struct redisClient { // ... // 事务状态 multiState mstate; /* MULTI/EXEC state */ // ... } redisClient;

事务状态包含一个事务队列, 以及一个已入队命令的计数器 (也可以说是事务队列的长度):

typedef struct multiState { // 事务队列,FIFO 顺序 multiCmd *commands; // 已入队命令计数 int count; } multiState;

事务队列是一个 multiCmd 类型的数组, 数组中的每个 multiCmd 结构都保存了一个已入队命令的相关信息, 包括指向命令实现函数的指针, 命令的参数, 以及参数的数量:

typedef struct multiCmd { // 参数 robj **argv; // 参数数量 int argc; // 命令指针 struct redisCommand *cmd; } multiCmd;

事务队列以先进先出(FIFO)的方式保存入队的命令: 较先入队的命令会被放到数组的前面, 而较后入队的命令则会被放到数组的后面。

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

redis> MULTI OK redis> SET "name" "Practical Common Lisp" QUEUED redis> GET "name" QUEUED redis> SET "author" "Peter Seibel" QUEUED redis> GET "author" QUEUED

那么服务器将为客户端创建事务状态:

最先入队的 SET 命令被放在了事务队列的索引 0 位置上。

第二入队的 GET 命令被放在了事务队列的索引 1 位置上。

第三入队的另一个 SET 命令被放在了事务队列的索引 2 位置上。

最后入队的另一个 GET 命令被放在了事务队列的索引 3 位置上。

3.5.4执行事务

当一个处于事务状态的客户端向服务器发送 EXEC 命令时, 这个 EXEC 命令将立即被服务器执行: 服务器会遍历这个客户端的事务队列, 执行队列中保存的所有命令, 最后将执行命令所得的结果全部返回给客户端。

EXEC 命令的实现原理可以用以下伪代码来描述:

def EXEC(): # 创建空白的回复队列 reply_queue = [] # 遍历事务队列中的每个项 # 读取命令的参数,参数的个数,以及要执行的命令 for argv, argc, cmd in client.mstate.commands: # 执行命令,并取得命令的返回值 reply = execute_command(cmd, argv, argc) # 将返回值追加到回复队列末尾 reply_queue.append(reply) # 移除 REDIS_MULTI 标识,让客户端回到非事务状态 client.flags &= ~REDIS_MULTI # 清空客户端的事务状态,包括: # 1)清零入队命令计数器 # 2)释放事务队列 client.mstate.count = 0 release_transaction_queue(client.mstate.commands) # 将事务的执行结果返回给客户端 send_reply_to_client(client, reply_queue)

3.5.5WATCH命令的实现

WATCH命令是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC执行后,检查被监视的键是否至少有一个被修改,如果是,服务器拒绝执行事务,并向客户端返回代表事务执行失败的回复。

/* Redis database representation. There are multiple databases identified * by integers from 0 (the default database) up to the max configured * database. The database number is the 'id' field in the structure. */ typedef struct redisDb { dict *dict; /* The keyspace for this DB 数据库键空间,保存数据库中所有的键值对*/ dict *expires; /* Timeout of keys with a timeout set 保存过期时间*/ dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */ dict *ready_keys; /* Blocked keys that received a PUSH 已经准备好数据的阻塞状态的key*/ dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS 事物模块,用于保存被WATCH命令所监控的键*/ // 当内存不足时,Redis会根据LRU算法回收一部分键所占的空间,而该eviction_pool是一个长为16数组,保存可能被回收的键 // eviction_pool中所有键按照idle空转时间,从小到大排序,每次回收空转时间最长的键 struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */ // 数据库ID int id; /* Database ID */ // 键的平均过期时间 long long avg_ttl; /* Average TTL, just for stats */ } redisDb;

在每个代表数据库的 server.h/redisDb 结构类型中, 都保存了一个 watched_keys 字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端。比如说,以下字典就展示了一个 watched_keys 字典的例子:

每个key后挂着监视自己的客户端。

3.5.6监控的触发

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

/* "Touch" a key, so that if this key is being WATCHed by some client the * next EXEC will fail. */ // Touch 一个 key,如果该key正在被监视,那么客户端会执行EXEC失败 void touchWatchedKey(redisDb *db, robj *key) { list *clients; listIter li; listNode *ln; // 字典为空,没有任何键被监视 if (dictSize(db->watched_keys) == 0) return; // 获取所有监视这个键的客户端 clients = dictFetchValue(db->watched_keys, key); // 没找到返回 if (!clients) return; /* Mark all the clients watching this key as CLIENT_DIRTY_CAS */ /* Check if we are already watching for this key */ // 遍历所有客户端,打开他们的 REDIS_DIRTY_CAS 标识 listRewind(clients,&li); while((ln = listNext(&li))) { client *c = listNodeValue(ln); // 设置CLIENT_DIRTY_CAS标识 c->flags |= CLIENT_DIRTY_CAS; } }

3.5.7事务的ACID性质

在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的安全性。

redis事物总是具有前三个性质。

a)原子性atomicity:redis事务保证事务中的命令要么全部执行要不全部不执行。

但是redis不同于传统关系型数据库,不支持回滚,即使出现了错误,事务也会继续执行下去。

b)一致性consistency:redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结。

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

入队错误

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

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

执行错误

如果命令在事务执行的过程中发生错误,比如说,对一个不同类型的 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 工具将部分成功的事务命令移除之后,才能再次启动服务器。还原之后的数据总是一致的,而且数据也是最新的(直到事务执行之前为止)。

c)隔离性Isolation:redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式,可以保证命令执行过程中不会被其他客户端命令打断。

因为redis使用单线程执行事务,并且保证不会中断,所以肯定有隔离性。

d)持久性Durability:持久性是指:当一个事务执行完毕,结果已经保存在永久介质里,比如硬盘,所以即使服务器后来停机了,结果也不会丢失

redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。

3.5.8重点提炼

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

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

事务在执行过程中不会被中断, 当事务队列中的所有命令都被执行完毕之后, 事务才会结束。

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

只有在客户端的 REDIS_DIRTY_CAS 标志未被打开时, 服务器才会执行客户端提交的事务, 否则的话, 服务器将拒绝执行客户端提交的事务。

Redis 的事务总是保证 ACID 中的原子性、一致性和隔离性, 当服务器运行在 AOF 持久化模式下, 并且 appendfsync 选项的值为 always 时, 事务也具有耐久性。

以上就是 Redis 客户端和服务器执行命令请求的整个过程了。

3.6、发布和订阅

3.6.1频道的订阅和退订

当一个客户端执行 SUBSCRIBE 命令, 订阅某个或某些频道的时候, 这个客户端与被订阅频道之间就建立起了一种订阅关系。

Redis 将所有频道的订阅关系都保存在服务器状态的 pubsub_channels 字典里面, 这个字典的键是某个被订阅的频道, 而键的值则是一个链表, 链表里面记录了所有订阅这个频道的客户端:

struct redisServer { // ... // 保存所有频道的订阅关系 dict *pubsub_channels; // ... };

每当客户端执行 SUBSCRIBE 命令, 订阅某个或某些频道的时候, 服务器都会将客户端与被订阅的频道在 pubsub_channels 字典中进行关联。

根据频道是否已经有其他订阅者, 关联操作分为两种情况执行:

如果频道已经有其他订阅者, 那么它在 pubsub_channels 字典中必然有相应的订阅者链表, 程序唯一要做的就是将客户端添加到订阅者链表的末尾。

如果频道还未有任何订阅者, 那么它必然不存在于 pubsub_channels 字典, 程序首先要在 pubsub_channels 字典中为频道创建一个键, 并将这个键的值设置为空链表, 然后再将客户端添加到链表, 成为链表的第一个元素。

SUBSCRIBE 命令的实现可以用以下伪代码来描述:

def subscribe(*all_input_channels): # 遍历输入的所有频道 for channel in all_input_channels: # 如果 channel 不存在于 pubsub_channels 字典(没有任何订阅者) # 那么在字典中添加 channel 键,并设置它的值为空链表 if channel not in server.pubsub_channels: server.pubsub_channels[channel] = [] # 将订阅者添加到频道所对应的链表的末尾 server.pubsub_channels[channel].append(client)

UNSUBSCRIBE 命令的行为和 SUBSCRIBE 命令的行为正好相反 —— 当一个客户端退订某个或某些频道的时候, 服务器将从 pubsub_channels 中解除客户端与被退订频道之间的关联:

程序会根据被退订频道的名字, 在 pubsub_channels 字典中找到频道对应的订阅者链表, 然后从订阅者链表中删除退订客户端的信息。

如果删除退订客户端之后, 频道的订阅者链表变成了空链表, 那么说明这个频道已经没有任何订阅者了, 程序将从 pubsub_channels 字典中删除频道对应的键。

UNSUBSCRIBE 命令的实现可以用以下伪代码来描述:

def unsubscribe(*all_input_channels): # 遍历要退订的所有频道 for channel in all_input_channels: # 在订阅者链表中删除退订的客户端 server.pubsub_channels[channel].remove(client) # 如果频道已经没有任何订阅者了(订阅者链表为空) # 那么将频道从字典中删除 if len(server.pubsub_channels[channel]) == 0: server.pubsub_channels.remove(channel)

3.6.2模式的订阅和退订

前面说过,服务器将所有频道的订阅关系保存起来,与此类似,服务器也将所有模式的订阅关系存在了pubsub_Patterns属性里。

struct redisServer { // ... // 保存所有频道的订阅关系 list *pubsub_patterns; // ... };

pubsub_Patterns属性是一个链表,每个结点是被订阅的模式,节点内记录了模式,节点内的client属性记录了订阅模式的客户端。

typedef struct pubsubPattern{ //订阅模式的客户端 redisClient *client; //被订阅的模式 robj *pattern; }pubsubPattern;

每当客户端执行PSUBSCRIBE这个命令来订阅某个或某些模式时,服务器会对每个被订阅的模式执行下面的操作:

1)新建一个pubsubPattern结构,设置好两个属性

2)将新节点加到pubsub_patterns尾部

伪代码实现:

def osubscribe(*all_input_patterns): #遍历所有输入的模式 #记录被订阅的模式和对应的客户端 pubsubPattern=create() pubsubPattern.client=client pubsubPattern.pattern=pattern #插入链表末尾 server.pub_patterns.append(pubsubPattern)

模式退订命令PUNSUBSCRIBE是PSUBSCRIBE的反操作

服务器将找到并删除那些被退订的模式

伪代码如下:(我想吐槽一下这样时间复杂度。。。没有更好的办法吗?)

def osubscribe(*all_input_patterns): #遍历所有退订的模式 for pattern in all_input_patterns: #遍历每一个节点 for pubsubPattern in server.pubsub_patterns: #如果客户端和模式都相同 if client==pubsubPattern.client: if pattern==pubsubPattern.pattern: #删除 server.pub_patterns.remove(pubsubPattern)

3.6.3、发送消息

当一个客户端执行PUBLISH 命令将消息发送给频道时,服务器需要:

1)把消息发送给所有本频道的订阅者

具体做法就是去pubsub_channels字典找到本频道的链表,也就是订阅名单,然后发消息

2)将消息发给,包含本频道的所有模式中的所有订阅者

具体做法就是去pubsub_patterns查找包含本频道的模式,并且把消息发送给订阅它们的客户端。

《三次给你聊清楚Redis》之彻底把单机聊透

3.6.4、查看订阅信息

redis2.8新增三个命令,用来查看频道和模式的相关信息。

PUBLISH CHANNELS[pattern]用于返回服务器当前被订阅的频道,pattern可写可不写,不写就查看所有,否则查看与pattern匹配的对应频道

这个子命令是通过遍历pubsub_channels字典实现的。

PUBLISH NUMSUB[CHANNEL-1 CHANNEL-2.....]返回这些频道的订阅者数量

这个子命令是通过遍历pubsub_channels字典,查看对应链表长度实现的。

PUBLISH NUMPAT返回被订阅模式数量

这个子命令是通过返回pubsub_patterns的长度实现的。

总而言之,PUBSUB 命令的三个子命令都是通过读取 pubsub_channels 字典和 pubsub_patterns 链表中的信息来实现的。

Redis 数据结构

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:大数据场景下的分布式系统性能优化之道
下一篇:十万字cpp成神总结-看完月薪25k
相关文章