一.什么是事务的四特征
- 原子性(Atomicity,或称不可分割性)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
接下来,我们将对四大特性的具体概念以及其底层实现原理来进行剖析:
在讲述具体的四大特性之前,我们先补充一点前置知识 :
1.逻辑架构和存储引擎
如上图,我们可以将mysql服务器的逻辑架构整体分为三层:
①第一层:负责客户端的连接和授权认证等
②第二层:服务器层:负责查询语句的解析,优化以及缓存,内置函数的实现和存储等
③第三层:存储引擎:负责数据库中数据的存储和读取,mysql服务器层不管理事务,事务是由存储引擎实现的。其中支持事务的存储引擎有innoDB和NDB Cluster等,其中比较应用广泛的是innoDB。
我们进行四大特性的具体介绍:
一.原子性
1. 定义
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
2.实现原理:redolog
在具体讲述redolog日志之前,我们先对mysql中存在的事务日志进行解释:mysql中存在很多类型的事务日志:包括二进制日志,错误日志以及查询日志等。除此之外,innoDB还提供了两个的日志:undolog(回滚日志) 和redolog(重做日志),其中undolog是数据的原子性和一致性的重要保证,而redolog用于保证事务的持久性。
UNDOLOG
undolog是实现事务原子性的重要保证,undolog能使一个事务中已经执行成功的所有sql语句进行回滚操作,其具体的工作流程如下:当事务修改数据库中的数据时,innoDB会对应生成具体的UNDO log ,一旦事务执行失败或者触发rollback操作时,就能使用redolog中的信息对数据库修改之前的数值进行恢复。
undolog是一种逻辑性日志,在触发rollback或者事务执行失败时,innoDB会根据undolog中记录的信息对数据库进行相反方向的操作,比如:之前的操作时insert语句,这时调用执行delete语句;如果之前执行的update语句,则会执行反方向的update语句。
二.持久性
1. 定义
持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
2.实现原理
redolog
在进行事务持久性的讲述之前,我们首先来说一下redolog存在的背景以及其存在的必要性:
对数据库的读写操作是对数据库中的数据进行操作,也就是说需要对数据库进行IO操作,但是对数据库频繁的IO效率很低,为此,innoDB提供了缓存层(BufferPool),BufferPool中储存着数据库部分数页的映射,作为数据库数据的缓冲:当我们需要从数据库汇总读入数据时,这时我们先从BufferPool中查找,在BufferPool中找不到,这时候从数据库中获取数据然后将其放入BufferPool,往数据库中写入数据也是先将数据写入到BufferPool,然后定期刷新到磁盘中(这一过程称为脏刷)。
但是带来便利的同时也存在一定的风险和弊端,如果在某刻数据库突然宕机,而这时在BufferPool中仍存在数据或者修改的数据没有刷入磁盘中,必然会导致数据丢失的问题,也无法保证数据的持久性,为了解决这个问题,redolog日志应运而生。redolog同样也是innoDB中的日志,它的实现原理如下:当数据写入到BufferPool之前或者对BufferPool中的数据进行修改之前,首先会先在redolog中记录此次操作,当事务提交时,会使用fsync接口对redolog进行刷盘,而如果数据库发生宕机问题,数据库会读取redolog中的信息,对数据库的数据进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有数据在写入BufferPoo或者在BufferPool中修改数据之前,都会先写入redolog ,保证了数据不会因mysql宕机而丢失,从而保证了数据的持久性。
那么既然redolog也是在数据提交时将数据写入数据库,那么它为什么比通过BufferPool写入数据库的效率要高呢?
主要是以下两方面的原因:
1.redolog是顺序IO,而BufferPool是随机IO,进行数据读写的位置随机生成,速度相对顺序IO较慢
2.BufferPool写入数据的方式是以一个数据页为单位,一般mysql的page大小为为16kb,而一旦一个页有任何数据的修改,都需要整页数据重写写入,而redolog是真正的有效写入:只写入新添加的或者修改的数据,无效IO大大减少。
三.隔离性
1.定义:
隔离性是研究事务之间的相互影响,隔离性是指事务内部的操作和其他事务之间是隔离的,在并发环境中,各个事务互不干扰,严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
2.实现原理
隔离性追求的是并发事务中彼此之间不相互影响,而在我们日常的操作中最主要考虑的是读操作和写操作:
1.一个写操作对另一个写操作的影响:通过锁机制进行解决
2.一个写操作对另一个读操作的影响:通过MVCC机制进行解决
1.锁机制:在写操作要求中,在同一时间只能允许一个事务对同一部分数据进行写操作,innoDB实现的锁机制的实现原理可以这样理解:事务在对数据进行写操作之前,要先获取锁资源,然后此时才能对数据进行写操作,其他事务想要获取锁资源必须等待当前事务进行事务的回滚或者提交写操作之后释放锁。
行锁和表锁:按照锁的粒度,可以将锁分为行锁和表锁以及介于两者之间的锁,表锁是在事务进行操作数据时锁定整张表,进行数据的操作时并发性能差,而行锁是在事务操作数据时只锁定被操作的数据,并发性能好,但是考虑到锁的创建,检查以及销毁都需要消耗资源,所以一般而言,表锁相对于行锁能节省部分资源,但是考虑到业务和性能需求,所以在一般情况下都使用行锁,但是sql中不同的存储引擎对表锁和行锁的支持也有所不同,对于innoDB而言,其支持行锁和表锁。
有关事务的隔离性以及不同隔离性可能会产生的问题,推荐大家看我另一篇文章,在这里就不进重复的赘述了:
关于对事务隔离性的深入理解_努力努力再努力mlx的博客-CSDN博客
2.MVCC机制:在sql的隔离级别中默认的隔离级别是可重复读(Repeatly Read),一般而言,RR不能解决幻读的问题,但是innoDB实现的RR能够避免幻读问题的产生,RR解决脏读、不可重复读、幻读等问题,使用的是MVCC:MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。MVCC具有以下特点:在同一时间,不同事务所读取的数据可能是不同的(不同的版本中的数据不同),如下图能比较好的体现这一特点:在T5时刻,事务A和事务C可以读取到不同版本的数据。
MVCC最大的好处是可以不加读锁,因此读写不冲突,而innoDB实现的MVCC,可以允许多个版本共存,其功能的实现主要是基于以下的技术和 数据结构:
1.隐藏列:数据库中的每条数据都有隐藏列,隐藏列有指向本行数据事务的id和指向undolog的指针
2.基于undolog的版本链:每条数据的隐藏列中都有指向undolog的指针,而每条undolog的指针也会指向更早版本的undolog,从而形成一条undolog版本链
3.ReadView:通过隐藏列和版本链,能将数据恢复到之前的版本,但是具体要恢复到哪个版本,则需要具体的ReadView来确定。所谓的ReadView ,是指事务(事务A)在某一时刻对整个事务系统(trx_sys)打快照,等之后再进行读操作时,会将读取到的事务id和trx_sys作比较,从而判断想读取的数据对该ReadView是否有效,即对事务A是否有效。
trx_sys中的主要内容,以及判断可见性的规则如下:
low_limit_id:表示生成的ReadView 中系统应分配给事务的下一个id.如果事务的id大于等于该low_limit_id,则对该ReadView不可见。
up_limit_id:表示在生成ReadView时在系统中活跃的事务,如果活跃事务的id小于up_limit_id,,则对该ReadView 可见
rw_trx_ids:表示在生成ReadView时在系统中活跃事务的id列表,如果查询数据的id在low_limit_id和up_limit_id之间,则需要看事务是否在rw_trx_ids中,如果在,则说明生成ReadView时事务仍在活跃中,则对该ReadView不可见,如果不在,说明生成ReadView时事务已经提交了,因此数据对ReadView可见。
3.MVCC是如歌规避脏读、不可重复读和幻读等问题的呢?
3.1脏读:
当事务A在T3时刻读取zhangsan的余额前,会生成ReadView,由于此时事务B没有提交仍然活跃,因此其事务id一定在ReadView的rw_trx_ids中,因此根据前面介绍的规则,事务B的修改对ReadView不可见。接下来,事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100。这样事务A就避免了脏读。
3.2不可重复读
当事务A在T2时刻读取zhangsan的余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。
当事务A在T5时刻再次读取zhangsan的余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见;因此事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100,从而避免了不可重复读。
3.3幻读
MVCC避免幻读的机制与避免不可重复读非常类似。
当事务A在T2时刻读取0<id<5的用户余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。
当事务A在T5时刻再次读取0<id<5的用户余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见。因此对于新插入的数据lisi(id=2),事务A根据其指针指向的undo log查询上一版本的数据,发现该数据并不存在,从而避免了幻读。
加锁读在查询时会对查询的数据加锁(共享锁或排它锁)。由于锁的特性,当某事务对数据进行加锁读后,其他事务无法对数据进行写操作,因此可以避免脏读和不可重复读。而避免幻读,则需要通过next-key lock。next-key lock是行锁的一种,实现相当于record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock的功能),还会锁定一个范围(gap lock的功能)。因此,加锁读同样可以避免脏读、不可重复读和幻读,保证隔离性。
四.一致性
1.概念:致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。
2.实现:一致性是数据库最终追求的目的,原子性,隔离性和持久性,都是为了满足一致性而存在的,除了数据库层面用于保证数据的一致性,一致性的实现在应用层也有所保障。
实现一致性的措施:
1.使用原子性,持久性和隔离性保证一致性,如果这三个特征无法保证,一致性也无法保证
2.数据库本身做出保障,例如不允许向整形数据中插入字符串信息,字符串的长度不允许超过列的最大长度
3.应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致性
五.总结:
- 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log
- 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
- 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
- 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障