事务
所谓事务(Transaction) ,是指作为单个逻辑工作单元执行的一系列操作
ACID回顾
-
Atomicity(原子性): 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
Redis:一个队列中的命令 执行或不执行 不会全部执行或全部不执行
-
Consistency(一致性): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
Redis: 集群中不能保证时时的一致性,只能是最终一致性
-
Isolation(隔离性): 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。 Redis: 单进程单线程,命令是顺序执行的,在一个事务中,会执行其他客户端的命令
-
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
Redis有持久化但不保证数据的完整性
Redis事务
-
Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。
-
Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
-
Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
-
Redis不支持回滚操作
事务命令
multi用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列
exec:执行命令队列
discard:清除命令队列
watch:监视key
unwatch:清除监视key
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 222
QUEUED
127.0.0.1:6379> hset set1 name zhangfei
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s2 333
QUEUED
127.0.0.1:6379> hset set2 age 23
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379> watch s1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set s1 555 # 此时其他客户端修改了s1
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get s1
222
127.0.0.1:6379> unwatch
OK
事务实现机制
事务的执行
-
事务开始
MULTI
命令的执行标志着事务的开始执行完该命令,redis.h的client结构体中的flags(用来表示是否在事务中)标识为
REDIS_MULTI
,表示该客户端从非事务状态切换至事务状态。 -
命令入队
当客户端处于非事务状态,命令会立即被服务器执行;
客户端处于事务状态,服务器是否立即执行分为2种情况:
-
EXEC,DISCARD,WATCH,MULTI 四个命令中任意一个都会立即执行
-
除以上4个命令,其他命令会被放入一个
FIFO
队列中,此时命令均不会被执行,等待事务执行命令发起后按序执行
-
-
执行事务 /事务丢弃
exec命令发起,RedisServer会遍历事务队列,执行队列中的命令,最后将执行的结果一次性返回给客户端。
事务丢弃: discard命令,redis会清空命令队列,将客户端事务标记取消。
异常处理
在事务执行过程中,一般会有 入队错误,执行错误两种错误存在。
-
入队错误: Redis对入队命令进行检查,有异常则直接返回提示,并将flags置为REDIS_DIRTY_EXEC,提交时拒绝队列命令,将会失败返回。
-
执行错误:对于逻辑错误,如类型错误等,提交命令时只对错误的执行提交失败,其他正常命令正常执行
源码数据结构
-
typedef struct client{
//状态,可以是单个或多个标志,也可以是记录客户端所处的状态
int flags,
// 事务状态,记录事务状态是exec还是multi
multiState mstate;
// watch操作后被监视的键
list *watched_keys;
}client;// 事务状态
typedef struct multiState{
// 事务队列,FIFO顺序
// 是一个数组,先入队的命令在前,后入队在后
multiCmd *commands;
// 已入队命令数
int count;
}multiState;// 事务队列
typedef struct multiCmd{
// 参数robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
}multiCmd;
Watch的执行
使用WATCH命令监视数据库键
redisDb有一个watched_keys字典,key是某个被监视的数据的key,值是一个链表,记录了所有监视这个数据的客户端。
监视机制的触发: 当修改数据后,监视这个数据的客户端的flags置为REDIS_DIRTY_CAS
事务执行: 客户端发送exec命令,服务器判断该客户端的flags,如果为REDIS_DIRTY_CAS,则清空事务队列。
typedef struct redisDb{ // 正在被WATCH命令监视的键 dict *watched_keys; // ..... }redisDb;
Redis的弱事务性
原子性
如果事务正常执行,没有发生任何错误,那么MULTI和EXEC配合使用,就可以保证多个操作都完成。
如果事务执行发生错误了,原子性
还能保证吗?
需要分三种情况来看: 语法错误,运行错误,Redis实例发生故障。
-
Redis语法错误
命令入队时,Redis 就会报错并且记录下这个错误,但此时,还能继续提交命令操作。 执行 EXEC 命令后,Redis 就会拒绝执行所有提交的命令操作,返回事务失败的结果。
这样一来,事务中的所有命令都不会再被执行了,保证了原子性。
127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 kkk QUEUED 127.0.0.1:6379> sets kkk sss (error) ERR unknown command 'sets' 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379>
-
Redis运行错误
事务操作入队时,命令和操作的数据类型不匹配,此时Redis实例没有检查出错误。
执行EXEC命令后,Redis实际执行就会报错。
此时,Redis对错误命令报错,但队列里正确的命令会执行!
这种情况下就是非原子性的,不支持回滚
127.0.0.1:6379> multi OK 127.0.0.1:6379> hset k1 hk1 v1 QUEUED 127.0.0.1:6379> set k2 v2 QUEUED 127.0.0.1:6379> exec 1) (error) WRONGTYPE Operation against a key holding the wrong kind of value 2) OK
-
Redis实例发生故障
在执行事务的 EXEC 命令时,Redis 实例发生了故障,导致事务执行失败。
如果 Redis 开启了 AOF 日志,那么,只会有部分的事务操作被记录到 AOF 日志中。 需要使用 redis-check-aof 工具检查 AOF 日志文件,把未完成的事务操作从 AOF 文件中去除。 这样一来,我们使用 AOF 恢复实例后,事务操作不会再被执行,从而保证了原子性。
当然,如果 AOF 日志并没有开启,那么实例重启后,数据也都没法恢复了,此时,也就谈不上原子性了。
Redis不支持事务回滚(为什么呢) 1、大多数事务失败是因为语法错误或者类型错误,这两种错误,在开发阶段都是可以预见的 2、Redis为了性能方面就忽略了事务回滚。 (回滚记录历史版本)
一致性
事务的一致性保证会受到错误命令、实例故障的影响。所以,按照命令出错和实例故障的发生时机,分成三种情况来看。
情况一:命令入队时就报错在这种情况下,事务本身就会被放弃执行,所以可以保证数据库的一致性。
情况二:命令入队时没报错,实际执行时报错在这种情况下,有错误的命令不会被执行,正确的命令可以正常执行,也不会改变数据库的一致性。
情况三:EXEC 命令执行时实例发生故障在这种情况下,实例故障后会进行重启,这就和数据恢复的方式有关了,我们要根据实例是否开启了 RDB 或 AOF 来分情况讨论下。
如果我们没有开启 RDB 或 AOF,那么,实例故障重启后,数据都没有了,数据库是一致的。
如果使用了 RDB 快照,因为 RDB 快照不会在事务执行时执行,所以,事务命令操作的结果不会被保存到 RDB 快照中,使用 RDB 快照进行恢复时,数据库里的数据也是一致的。
如果使用了 AOF 日志,而事务操作还没有被记录到 AOF 日志时,实例就发生了故障,那么,使用 AOF 日志恢复的数据库数据是一致的。
如果只有部分操作被记录到了 AOF 日志,可以使用 redis-check-aof 清除事务中已经完成的操作,数据库恢复后也是一致的。
总结来说,在命令执行错误或 Redis 发生故障的情况下,Redis 事务机制对一致性属性是有保证的。
隔离性
事务的隔离性保证,会受到和事务一起执行的并发操作的影响。
事务执行又可以分成命令入队(EXEC 命令执行前)和命令实际执行(EXEC 命令执行后)两个阶段,所以,就针对这两个阶段,分成两种情况来分析:
并发操作在 EXEC 命令前执行,一个事务的 EXEC 命令还没有执行时,事务的命令操作是暂存在命令队列中的。此时,如果有其它的并发操作,需要看事务是否使用了 WATCH 机制。Watch机制会先检查监控的键是否被其他客户端修改了,如果修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户端可以再次执行事务,此时,如果没有并发修改事务数据的操作,事务就能正常执行,隔离性也得到了保证。
并发操作在 EXEC 命令后执行,此时,隔离性可以保证。并发操作在 EXEC 命令之后被服务器端接收并执行。因为 Redis 是用单线程
执行命令,而且,EXEC 命令执行后,Redis 会保证先把命令队列中的所有命令执行完。
持久性
因为 Redis 是内存数据库,所以,数据是否持久化保存完全取决于 Redis 的持久化配置模式。
如果 Redis 没有使用 RDB 或 AOF,那么事务的持久化属性肯定得不到保证。
如果 Redis 使用了 RDB 模式,那么,在一个事务执行后,而下一次的 RDB 快照还未执行前,如果发生了实例宕机,这种情况下,事务修改的数据也是不能保证持久化的。
如果 Redis 采用了 AOF 模式,因为 AOF 模式的三种配置选项 no、everysec 和 always 都会存在数据丢失的情况
所以,事务的持久性属性也还是得不到保证。不管 Redis 采用什么持久化模式,**事务的持久性属性是得不到保证的。