Redis(三十七):事务

Redis的事务包括multi、exec、watch和discard命令,保证命令顺序执行。事务开始后,命令入队,直到exec提交。WATCH命令用于监视键,若键在事务执行前被修改,事务将不安全。Redis事务不支持回滚,追求简单高效,错误处理倾向于执行而非回滚。在持久化方面,AOF的always模式能保证持久性,但其他模式可能无法确保。

Redis也是有事务功能的。

事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行过程中,是会阻塞服务的(这里是指事务在提交时,服务会阻塞的,不会去执行其他客户端提交的命令请求),服务器会优先将事务执行完毕,才会去执行其他客户端传来的命令请求

Redis对事务有关的命令为

  • multi:显示开启一个事务
  • exec:结束事务
  • watch:监视指定key
  • discard:中途取消事务

事务的实现

事务从开始到结束需要去经历三个过程

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

事务开始

事务开始也就是需要显示地去开启一个事务,所以需要使用multi命令

该命令的作用其实就是将客户端的状态从非事务状态切换至事务状态,这一切换是通过修改客户端状态的flags属性为REDIS_MULTI完成的,修改之后服务器会返回ok回复

命令入队

在这里插入图片描述
从上图中可以看到,当进入事务状态时,键值对命令的回复不再是ok,而是一个queued,代表命令入队,命令入队与普通执行命令不一样,普通执行命令是马上进行处理,而命令入队却要分情况

  • 如果客户端发来的命令是exec、discard、watch、multi,那么就会立即去执行,然后返回回复信息
  • 如果客户端发来的命令并不是上面的4种,那么服务器并不会立即去执行这个命令,而是将命令放入一个事务队列里面,然后返回一个QUEUED回复,代表入队成功
事务队列

上面已经说到过,除了关于事务的命令,其他命令都会进入事务队列。

使用队列,是因为要保证命令的执行顺序

事务队列是保存在客户端状态里面的,在客户端状态里面有一个mstate属性,该属性是一个miltiState结构,这个结构里面就是保存着事务队列,还有一个已经入队命令的计数器

typedef struct redisClient(
	//...
    //事务状态
    multiState mstate;
    //...
)redisClient;
typedef struct multiState(
	//事务队列
    multiCmd *commands;
    //已入对命令计数
)multiState;

可以看到,队列中的每一个任务都是一个multiCmd

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

可以看到,里面保存了参数、参数个数还有命令指针

之前已经学习过,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;

事务的整体过程已经解释完了。

整个过程就是

  1. 服务端接收到客户端的multi命令,将客户端标记为事务状态
  2. 客户端发送键值对命令给服务端,服务端检测到客户端是事务状态,将命令保存到客户端里面的命令队列里面
  3. 客户端发送exec命令,就要进行提交事务,通过遍历命令队列,将回复一一保存在回复队列里面,命令执行完后,就将回复队列返回给客户端

WATCH命令的实现

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命令之前,而且事务只会在提交时发生阻塞,命令入队一半不会阻塞,所以在提交之前,其他客户端是可以对任意键值对操作的,所以就会发生事务不安全,因为事务里已经不满足一致性和隔离性了

事务执行失败的栗子

在这里插入图片描述

事务的ACID性质

  • 原子性
  • 一致性
  • 隔离性
  • 持久性

具体是怎样就不赘述了

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时,程序会交由操作系统来决定合适将命令数据持久化进磁盘,所以也是不可以保证持久性的

有一个可以保证持久化的方法,就是在每条事务后面加上SAVE命令,不过这种做法的效率太低了,并不具有实用性。

Redis为何不支持回滚

Redis与一些传统的关系型数据库事务的最大区别就是Redis的事务是不支持回滚的

体现在,即使事务里面的命令出现错误,也不会中止事务,而是会继续执行下面的命令

Redis的作者在事务功能的文档中解释说,不支持事务回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不相符,并且他认为,Redis事务的执行错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,在生产环境中很少出现,最终他认为没有必要进行回滚。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值