文章目录
Redis - 事务机制
1.传统关系型数据库事务
1.1 事务基本概念
说到事务那就不得不提一下大家都十分熟悉的传统关系型数据库中的事务机制。一个数据库事务通常包含了一系列对数据库的读/写操作。而事务的存在主要包含以下两个目的:
- 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,防止彼此操作的互相干扰。
- 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
1.2 ACID(事务四大特性)
说到数据库事务,这里我们就必须了解他的几个基本要求,也就是它的四大特性:
- 原子性(Atomicity):事务作为一个整体被执行,包含的所有对数据库的操作要么全部执行成功,要么全部执行失败回滚。
- 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。即数据库的完整性约束没有被破坏。比如两个用户之间相互转账,两人相加有5000,无论进行多少次转账操作或者目标和来源变换,最终两者加在一起都保持5000
- 隔离性(Isolation):多个事务并发执行时,不同事务之间不存在相互干扰。
- 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。
1.3 事务并发问题
上面我们了解了事务的四大特性,当事务并发时从隔离机制上来分析,可能存在以下问题:
- 脏读:脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。比如事务A读取了事务B更新但未提交的数据,若事务B发生错误回滚操作,那么事务A读取到的数据则是脏数据。
- 不可重复读:不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。比如事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时结果不一致。部分情况下可以通过加锁或者MVCC避免该情况的发生。
- 幻读(虚读):在一个事务中,同一个查询多次返回的结果不一致。比如事务A对一批数据进行了处理,事务B此时插入了一条属于这一批数据同类型的数据,事务A处理数据后再次查询发现多了一条记录,认为自己产生了幻觉一样。
这里需要注意的是,幻读是由于并发事务变更记录数量导致的,不能像不可重复读通过操作数据加锁解决,因为对于新增的数据根本无法加锁,只有将事务串行化才能避免幻读。幻读和不可重复读都是读取了另一个已经提交的事务(这点与脏读不同),所不同的是不可重复读查询的都是同一个数据项(某条数据),而幻读针对的是一批数据整体(比如数据的个数),也就是说解决不可重复读只要锁行而解决幻读需要锁表。
1.4 事务隔离级别
这里我们了解来回顾一下关系型数据库Mysql中的事务隔离级别。
- 读未提交(read-uncommitted):最低的隔离级别,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。
- 读已提交(read-committed):只有在事务提交后,其更新结果才会被其他事务知晓。可解决脏读问题。
- 可重复读(repeatable-read):在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。Mysql默认隔离级别。
- 串行化(serializable):事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。
隔离级别越高,牺牲的性能越大,Mysql默认选取的隔离级别为可重复读(repeatable-read),对于数据的正确性和安全性以及系统性能的选择需要根据自己实际需求考虑。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | √ | √ | √ |
读已提交(read-committed) | × | √ | √ |
可重复读(repeatable-read) | × | × | √ |
串行化(serializable) | × | × | × |
上面表格将各隔离级别是否会发生某些问题进行了整理,根据具体需求我们也可以根据以下M命令选择修改默认隔离级别:
语法:set global transaction isolation level read uncommitted;
种类:read uncommitted
、read committed
、repeatable read
、serializable
1.5 MVCC技术
关于Mysql如何保证以上隔离级别的实现,这里就要引入一个概念-
多版本并发控制(Multi-Version Concurrency Control)
。在我们对Mysql的数据结构稍作了解之后就会发现,数据库对每行数据记录不仅仅局限于本身的表数据结构,还包含了以下几列自身功能的属性:
- DATA_TRX_ID:记录Transcation ID,每个事务开启处理都会在其已经存在的事务ID上自增。
- DATA_ROLL_PTR:存储undo_log指针,undo_log用来记录事务中的执行命令。
- DELETE_BIT:标识该记录是否被删除,真正删除操作会在commit事务时执行。
除此之外每一行数据中还额外保存两个隐藏的列:当前行创建时的版本号和删除时的版本号。这里的版本号并不是实际的时间值,而是系统版本号。每开始新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为当前事务的版本号, 用来和操作每行记录时的版本号进行比较。简单总结就是以下几个特征:
- 每行数据都存在一个版本号,每次数据更新时都更新该版本。
- 修改时记录下当前数据版本之后进行数据操作,各个事务之间不存在干扰。
- 保存时数据时比较当前版本号和之前记录版本号是否相同,如果成功则提交事务覆盖原记录,失败则回滚操作。
2.Redis事务机制
2.1 Redis事务相关命令
很多人都说NoSQL不支持事务,虽然的Transactions提供的事务并非严格的ACID,但还是提供了基本的命令打包执行功能。Redis事务其实大体上和Mysql事务功能相类似,对关系型数据库的事务有一定了解的同学,学习Redis事务也会十分简单。
2.1.1 multi/exec
以
multi
开启事务状态,然后将多个命令入队到事务当中, 最后由exec命令触发提交事务,一并执行事务中的所有命令。
这里我们下开启一个客户端开启事务并执行以下命令:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get msg
QUEUED
127.0.0.1:6379> set msg msg1
QUEUED
127.0.0.1:6379> get msg
QUEUED
此时我们暂时未提交事务,再开启另一个客户端不开启事务直接执行命令:
127.0.0.1:6379> set msg msg2
OK
127.0.0.1:6379> get msg
"msg2"
这里我们已经将
msg
对应内容置为了msg2
,此时提交第一个客户端的事务:
127.0.0.1:6379> exec
1) "msg2"
2) OK
3) "msg1"
这里可以看到,三条入队命令相继执行,并且我们第一条
get msg
命令获取到的值是我们开启事务后另一个客户端设置的内容,也就是说这三条命令其实是保存在了一个队列中并未真正去执行,这里我们再看下另一个客户端:
127.0.0.1:6379> get msg
"msg1"
一个情理之中的结果,也就是说Redis事务实际上就是将一系列的命令打包成一个队列,在提交时作为一个整体提交去执行的。
2.1.2 discard
discard
命令用于取消一个事务,它会清空客户端的整个事务队列中的命令,然后将客户端从事务状态调整回非事务状态,最后返回字符串 OK 给客户端,说明事务已被取消。
这里我们通过以下命令演示一下:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get msg
QUEUED
127.0.0.1:6379> set msg msg2
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get msg
"msg1"
在我们使用
discard
命令取消事务之后,再去获取数据可以发现仍然处于事务之前的值,也就是说事务当中的命令并未执行成功。
2.1.3 watch
watch
命令用于在事务开始之前监视任意数量的键,当调用exec
命令执行事务时,若任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行, 直接返回失败(nil)。
这里我们先对msg
这个数据进行监控,然后开启事务将命令推入队列:
127.0.0.1:6379> watch msg
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> get msg
QUEUED
127.0.0.1:6379> set msg msg3
QUEUED
此时我们再开启另一个客户端去修改
msg
内容:
127.0.0.1:6379> set msg msg4
OK
127.0.0.1:6379> get msg
"msg4"
修改成功后我们提交第一个客户端事务:
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get msg
"msg4"
提交事务失败,数据未根据队列中命令进行修改。
2.2 事务执行过程
这里我们可以通过下图对Redis的事务执行过程有一个大致的了解。
3.Redis事务与传统关系型事务的比较
通过上面的了解,我们对Redis事务和传统关系型事务都有了大致的一个概念,这里我们就从几个方面去分析一下两者的区别。
- 原子性(Atomicity):单个Redis命令的执行是具有原子性的,但Redis并没有在事务上增加任何维持原子性的机制,所以 Redis事务的执行并不是原子性的。如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功。
- 一致性(Consistency):
a) 入队错误:在命令入队的过程中,若入队命令是错误的,比如命令的参数数量错误、命令基本语法错误等,那么服务器将向客户端返回一个出错信息,并且将客户端的事务状态设为REDIS_DIRTY_EXEC,如有需要还需再次开启事务状态。127.0.0.1:6379> multi OK 127.0.0.1:6379> get msg QUEUED 127.0.0.1:6379> setmsg (error) ERR unknown command 'setmsg' 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors.
b) 执行错误:如果命令在事务执行的过程中发生错误,比如
incr
一个String类型的数据,那么Redis只会将错误包含在事务的结果中,并不会引起整个事务中断或失败, 所以它对事务的一致性也没有影响。127.0.0.1:6379> multi OK 127.0.0.1:6379> get name QUEUED 127.0.0.1:6379> set name kobe QUEUED 127.0.0.1:6379> incr name QUEUED 127.0.0.1:6379> exec 1) (nil) 2) OK 3) (error) ERR value is not an integer or out of range 127.0.0.1:6379> get name "kobe"
- 隔离性(Isolation):
watch
命令用于在事务开始之前监视任意数量的键,当调用exec
命令执行事务时,若任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行, 直接返回失败(nil)。- 持久性(Durability):Redis事务仅仅是用队列维护起了一组Redis命令,未提供任何额外的持久性功能,所以事务的持久性仍然由Redis本身使用的持久化策略决定。