之前已经学习过,Redis的命令是保存在服务器状态里面的命令字典里面,字典里面的值就是redisCommand。
执行事务
当服务器接收到客户端传来的exec命令之后,服务器就会遍历该客户端状态里面的事务队列,然后执行队列中保存的所有命令,最后是将命令所得的结果全部返回给该客户端,执行命令的顺序当然是按照队列先进先出的原则。
但这里要注意的是,命令执行完的结果是也是放在一个队列里面,执行完后要进行释放资源和修改对应客户端的状态
-
移除客户端状态的事务标识,也就是移除REDIS_MULTI标识,让客户端回到非事务的状态
-
对事务队列进行释放
-
对入队命令计数器进行清零
伪代码表示
def exec():
#创建空白的回复队列,用于保存命令的执行结果
reply_queue = []
#遍历事务队列中的每个项,这里要遍历的
for multiCmd in client.mstate.commands:
#使用队列项里的参数、参数个数和命令来执行命令
reply = execute_command(multiCmd.cmd,multiCmd.argv,multiCmd.argc)
#将返回值追加到回复队列里面
reply_queue.append(reply)
End For
#移除客户端状态的事务标识
#计算方法是与上事务标识的非
client.flags &= ~REDIS_MULTI;
#最后还要释放资源
#计数器清0
client.mstate.commands.argc = 0;
#队列释放
release_transaction_queue(client.mstate.commands);
#最后就是将回复队列返回给客户端
send_reply_to_client(client,reply_queue);
end exec;
事务的整体过程已经解释完了。
整个过程就是
-
服务端接收到客户端的multi命令,将客户端标记为事务状态
-
客户端发送键值对命令给服务端,服务端检测到客户端是事务状态,将命令保存到客户端里面的命令队列里面
-
客户端发送exec命令,就要进行提交事务,通过遍历命令队列,将回复一一保存在回复队列里面,命令执行完后,就将回复队列返回给客户端
WATCH命令是用来监视数据库键的
每个Redis数据库都保存着一个watched_keys的字典,这个字典的键是某个被WATCH命令监视的数据库键,而字典的值则是一个链表,保存着所有监视这个数据库键的客户端状态
这里要注意是,WATCH命令监视的键值对是保存在数据库状态中的
typedef struct redisDb(
//…
//正在被WATCH命令监视的键
dict *watched_keys;
//…
)redisDb;
通过该字典,服务器就可以知道哪些数据库键被哪些客户端监视中
客户端执行WATCH命令,那么服务端就会将其与对应的键值对关联起来,本质上就是在对应的数据库中的watch_keys字典中,先在字典中查找有没有改键值
-
如果有,证明已经有客户端在监视该键值对了,那么就在对应的链表后面加上此时要监视该键值对的客户端
-
如果没有,证明该客户端是第一个键值该键值对的客户端,需要在数组中新增该键值,然后初始化链表,然后往链表中增添该客户端状态
监视机制的触发
在服务器执行客户端传来的对键值对修改的命令,比如set、lpush、sadd等。。在执行之后都会调用touchWatchKey函数对watched_kets字典进行检查(这也是为什么被监视的键值对要放在数据库状态中,为了方便全局检查),看看字典中有保存着该键值,如果有,就证明有客户端正在监视这个键值对,那么就要对监视该键值对的客户端进行修改
对应的修改操作为,将监视该键值对的所有客户端的flags修改为REDIS_DIRTY_CAS标识,表示该客户端的事务安全性已经被破坏了,即事务不完整了,不可以提交
当服务器接收到一个客户端发来的EXEC命令之后,服务器会去看客户端对应的客户端状态是否打开了REDIS_DIRTY_CAS标识,也就是查看事务的安全性是否被破坏,分下面两种情况来进行讨论
-
如果客户端的REDIS_DIRTY_CAS标识已经被打开,那么说明客户端所监视的键当中,至少有一个键是被修改了(事务未提交前),事务已经不再安全了,所以服务器会拒绝执行客户端提交的事务
-
如果客户端的REDIS_DIRTY_CAS标识没有被打开,那么说明客户端监视的键都没有被修改过(事务未提交前),所示认定事务仍然是安全的,服务器会执行客户端的提交命令
为什么会出现事务不安全呢?
这里还要注意的是,WATCH命令是不可以在事务里面使用的,也就是说,必须用在Multi命令之前,而且事务只会在提交时发生阻塞,命令入队一半不会阻塞,所以在提交之前,其他客户端是可以对任意键值对操作的,所以就会发生事务不安全,因为事务里已经不满足一致性和隔离性了
-
原子性
-
一致性
-
隔离性
-
持久性
具体是怎样就不赘述了
Redis对于原子性的实现
事务中的命令只要有一个失败,那么所有命令都会执行失败的,也就是不允许事务提交
Redis对于一致性的实现
一致性是指没有非法或者无效的数据,也就是说要保持约束
Redis是通过谨慎的错误检测和简单的设计来保证事务的一致性
简单的设计就是指Redis数据库的键值对是没有关联性的,所以,一致性基本可以忽略。
入队错误
一个事务在入队命令的过程中,如果出现了命令不存在,或者命令的格式不正确,那么Redis会拒绝执行这个事务
执行错误
当事务在提交时,可能会发生命令执行错误,即使发生了错误,服务器也是不会去中断事务的执行,依然会去接着执行下面的命令
我们可以看到,msgTwo是一个字符串对象,使用了rpush命令对其操作,所以肯定会报错,但事务依然不会停止,msgFour依然创建成功了
但这样允许是不会改变Redis的一致性,因为前面已经提到过,Redis的键值对是没有关联的,所以一致性只是保证了自己键值对的一致,与其他键值对无关,键值对命令发送错误,不去修改,不就保证了一致性了嘛
服务器停机
服务器停机会产生以下几种情况
-
没有开启持久化操作,停机之后所有数据消失,重启之后数据库变为空白,都消失了,不就一致了
-
开启了持久化操作,无论是RDB还是AOF,恢复到的状态肯定是一致性的
Redis对于隔离性的实现
Redis是一个单线程,事务提交之后肯定都是串行执行的,会发生阻塞的,不允许并发提交,最重要的是,命令是入队执行的,不是马上执行的,要提交才会生效,所以隔离性是可以保证的。
Redis对于持久性的实现
持久性的实现完全靠持久化功能
-
没开持久化功能,事务提交之后,也没有持久化进磁盘,肯定不能保证
-
开了RDB持久化功能,RDB持久化是要满足一定条件才会生效的,而且使用的是BGSAVE,并不确保是否真的写进了磁盘,也是不可以保证持久性的
-
开了AOF持久化功能
-
如果appendfsync的选项为always时,程序总会在执行命令之后调用同步sync函数,将数据持久化进磁盘,这是可以保证持久性的
-
如果appendfsync为everysec时,程序是每一秒同步一次数据到磁盘中,可能会丢失这一秒的所有事务,所以,不可以保证持久性
-
如果appendfsync为no时,程序会交由操作系统来决定合适将命令数据持久化进磁盘,所以也是不可以保证持久性的