MySQL底层概述—9.ACID与事务

大纲

1.ACID之原子性

2.ACID之持久性

3.ACID之隔离性

4.ACID之一致性

5.ACID的关系

6.事务控制演进之排队

7.事务控制演进之排它锁

8.事务控制演进之读写锁

9.事务控制演进之MVCC

10.事务隔离级别之隔离级别的类型

11.事务隔离级别之和锁的关系

12.事务隔离级别之隔离级别的控制

1.ACID之原子性

在关系型数据库中,一个逻辑单元要成为事务,必须满足这4个特性:

一.原子性(Atomicity)

二.一致性(Consistency)

三.隔离性(Isolation)

四.持久性(Durability)

原子性:指的是事务是一个原子操作单元,它对数据的修改,要么全执行要么全不执行。

每写一个事务都会修改Buffer Pool,产生Redo、Undo日志。如果事务提交后redo log已刷入磁盘,此时机器恰好宕机了,那么就可以根据redo log恢复事务修改过的缓存数据。如果需要回滚事务,那么就可以基于undo log进行回滚,也就是通过undo log回滚事务之前对缓存页做的修改。所以undo日志实现了事务的原子性。

2.ACID之持久性

(1)update语句的执行流程

(2)两阶段提交

(3)为什么使用两阶段提交

(4)持久性的保证

持久性:指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不应该对其有任何影响,数据不会丢失。

MySQL持久性的保证依赖两个日志文件:redo log文件和binlog文件。最开始MySQL是没有InnoDB引擎的,MySQL自带的引擎是MyISAM引擎。但是MyISAM引擎没有CrashSafe能力,因为binlog日志只能用于归档。

如果InnoDB引擎只依靠binlog日志也是没有CrashSafe能力的。因为binlog日志是逻辑日志,不能直接应用到内存的缓存页。而且虽然binlog拥有归档日志,但是没有标志让InnoDB判断哪些数据已经刷盘。比如binlog日志是一条插入语句,如何确定这个插入语句是否已经刷盘成功。

所以InnoDB要通过redo log才能实现CrashSafe能力,redo日志文件中会存储一些信息比如checkpoint_no和checkpoint_lsn等。如果redo日志的LSN值小于checkpoint_lsn,则说明该redo日志已刷盘,其中checkpoint_lsn其实就是数据页的LSN。

(1)update语句的执行流程

下图为update语句的执行流程图:白色框表示在InnoDB内部执行,绿色框表示在执行器中执行。

mysql> update T set c = c + 1 where ID = 2;

步骤一:执行器先找引擎获取ID=2这一行数据

ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器。否则,需要先从磁盘读入内存,然后再返回。

步骤二:执行器拿到引擎给的行数据,把值加1

比如原来是N,现在就是N+1。于是会得到新的一行数据,接着再调用引擎接口写入这行新数据。

步骤三:引擎将新数据更新到Buffer Pool + 记录更新操作到redo log

此时redo log的状态处于prepare状态,引擎会告知执行器已执行完成,随时可以提交事务。

步骤四:执行器生成这个更新操作的binlog,并把binlog写入磁盘

步骤五:执行器调用引擎的提交事务接口

引擎会把redo log的状态修改成commit状态,用来表示更新已经完成。

(2)两阶段提交

将redo log的写入拆成了两个步骤:prepare和commit,这就是两阶段提交2PC。

(3)为什么使用两阶段提交

使用两阶段提交主要是用来解决binlog和redo log的数据一致性问题。由于InnoDB会先写redo log,再写binlog,如果没有两阶段提交,那么在主从架构下,如果主库的redo log写盘了,但是binlog没写,结果主库宕机了,此时只能靠从库恢复数据,那么就会出现数据不一致。

当进行崩溃恢复时,redo log和binlog有一个共同的数据字段,叫XID。崩溃恢复时会按顺序扫描redo log,如果碰到既有prepare又有commit的redo log,就直接提交。如果redo log处于prepare,则拿着XID去binlog判断对应binlog是否完整。如果binlog完整,则提交事务,如果binlog不完整,则回滚事务。

其实崩溃恢复只有三种情况:

情况一:在写入redo log之前崩溃

那么此时redo log和binlog都没有写,数据是一致的,崩溃也无所谓。

情况二:在写入redo log的prepare阶段后马上崩溃

那么在恢复时,由于redo log没有被标记为commit,于是会拿着redo log中的XID去binglog中查找,此时查不到便回滚。

情况三:在写入binlog后崩溃

那么在恢复时,拿着redo log中的XID就能找到对应的binglog,直接提交。

(4)持久性的保证

redo log在系统崩溃时,可修复数据,从而保障事务数据的持久性。

一.通过undo log实现的事务原子性可以保证逻辑上的持久性。

二.通过InnoDB存储引擎的数据刷盘可以保证物理上的持久性。

3.ACID之隔离性

隔离性:指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并发事务是隔离的。

不考虑隔离性可能会引发如下问题:

一.脏读:一个事务读取到了另一个事务修改但未提交的数据。未提交的数据后面回滚了,造成了使用到脏数据。

二.不可重复读:一个事务中多次读取同一行记录的结果不一致。后面读取的与前面读取的结果不一致。

三.幻读:一个事务中多次按相同条件查询的结果不一致。后续查询的结果和面前查询结果不同,多了或少了几行记录。

InnoDB支持的隔离性有4种,隔离性从低到高分别为:读未提交、读已提交、可重复读、可串行化。而通过锁(解决脏写) + MVCC(解决脏读 + 不可重复读 + 幻读)可以保障事务数据的隔离性。

4.ACID之一致性

一致性:指的是事务开始前和事务结束后,数据库的完整性限制未被破坏。原子性、持久性、隔离性共同保证了数据的一致性。

一致性包括约束一致性和数据一致性。

一.约束一致性

创建表结构时所指定的外键、Check、唯一索引等约束,不过在MySQL中并不支持Check。

二.数据一致性

这是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是单单依赖于某一种技术。

一致性可以理解为数据的完整性,数据的完整性是通过原子性、隔离性、持久性来保证的,而这3特性又是通过Redo/Undo来保证的。

一致性也可以理解为业务逻辑的一致性,业务逻辑上的一致性包括唯一索引、外键约束、check约束。

5.ACID的关系

事务的持久化是为了应对系统崩溃时造成的数据丢失问题,只有保证了事务的一致性,才能保证执行结果的正确性。

在非并发状态下,事务间天然保证隔离性。所以在非并发状态下,只需要保证事务的原子性即可保证一致性。在并发状态下,则需要严格保证事务的原子性、隔离性才能保证一致性。

6.事务控制演进之排队(锁的颗粒度是全局)

排队处理是管理事务的最简单方法。就是完全顺序执行所有事务的数据库操作,不需要加锁。

简单来说就是全局排队,序列化执行所有的事务单元,数据库在某个时刻只处理一个事务操作。特点是强一致性,处理性能低。

7.事务控制演进之排它锁(锁的粒度是具体数据项)

引入锁之后就可以支持并发处理事务。如果事务间涉及到相同的数据项时,会使用排他锁(或叫互斥锁)。

先进入的事务独占数据项后,其他事务被阻塞,等待前面的事务释放锁。

注意:在上述事务1结束之前,锁是不会被释放的,所以事务2必须等到事务1结束之后开始。

8.事务控制演进之读写锁(锁的颗粒度是某数据项的读写操作)

读和写的操作可以分为:读读、写写、读写、写读。读写锁就是进一步细化锁的颗粒度,让读和读之间不加锁,这样下面的两个事务就可以同时被执行了。

读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。

9.事务控制演进之MVCC

(1)MVCC概念

(2)undo log多版本链

(3)ReadView

(4)MVCC在MySQL中的具体实现

(5)SELECT、DELETE、 INSERT、 UPDATE语句对隐藏字段的处理

(6)MVCC读操作分类

(1)MVCC概念

MVCC(Multi Version Concurrency Control)被称为多版本并发控制。指的是在数据库中为实现高并发的数据访问,对数据进行多版本处理,并通过事务的可见性保证事务能看到自己应该看到的数据版本。

MVCC最大的好处是读不加锁,读写不冲突。在读多写少的系统应用中,读写不冲突能极大的提升系统的并发性能,这也是为什么现阶段几乎所有的关系型数据库都支持MVCC的原因。不过目前MVCC只在RC和RR两种隔离级别下工作。

(2)undo log多版本链

每条数据都有两个隐藏字段:事务ID(trx_id)和回滚指针(roll_pointer)。

事务ID(trx_id)表示最近一次更新这条数据的事务ID,回滚指针(roll_pointer)表示指向之前生成的undo log。当然可能还会有一个隐藏字段叫row_id,但没有主键ID时才会自动生成。

下面演示undo log多版本链的生成:

时间点1:事务A插入数据

假设有一个事务A(trx_id=50),向表中插入一条数据。插入的这条数据的值为A,该数据结构如下,其中roll_pointer会指向一个空的undo log:

时间点2:事务B修改事务A插入的数据

接着有一个事务B(trx_id=58)对事务A插入的数据进行修改,将值修改为B。事务B的ID是58,在更新前会生成一个undo log来记录之前的值,然后让这条数据的roll_pointer指向这个事务B生成的undo log回滚日志。

时间点3:事务C更新事务B修改后的数据

如果再有一个事务C(trx_id=69)继续更新该条记录值为C,则会跟时间点2的步骤一样。

总结:每一条数据都有多个版本,版本之间通过undo log链条进行连接。通过这样的设计,可以保证每个事务提交时一旦需要回滚操作,同一个事务只能读到比当前版本更早提交的值,不能看到更晚提交的值。

(3)ReadView

一.什么是Read View

二.Read View的使用

三.Read View的更新方式

四.Read Committed级别

五.Repeatable Read级别

六.Read View总结

七.生成Read View时机

接下来介绍基于undo log版本链实现的Read View视图机制。

一.什么是Read View

Read View是InnoDB在实现MVCC时用到的一致性读视图,一致性读视图也就是Consistent Read View。Read View是用来支持RC(读已提交)和RR(可重复读)隔离级别的实现。Read View简单理解就是对数据在每个时刻的状态拍成照片记录了下来,那么之后获取某时刻的数据时还可以是原来照片上的数据,是不会变的。

Read View中比较重要的字段有4个:

字段一:m_ids,用来表示MySQL中哪些事务正在执行但是没有提交。

字段二:min_trx_id,就是m_ids里最小的值。

字段三:max_trx_id,下一个要生成的事务ID值,也就是最大事务ID。

字段四:creator_trx_id,当前事务的ID。

二.Read View的使用

下面是一个Read View所存储的信息:

接下来是Read View的使用例子:

时间点1:

假设数据库有一行数据,很早就有事务操作过,事务ID是32。此时两个事务并发执行,一个事务A(trx_id=45),一个事务B(trx_id=59)。事务A需要读取数据,而事务B需要更新数据,如下图示。如果不加任何限制,这里会出现脏读的情况,也就是一个事务可能会读到另一个事务没有提交的值。

时间点2:

现在事务A开启一个ReadView,这个ReadView里的m_ids就包含了事务A和事务B的两个ID:45和59。然后min_trx_id就是45,max_trx_id就是60,creator_trx_id就是45,即事务A自己。

时间点3:

此时事务A第一次查询这行数据。首先会判断一下当前这行数据的txr_id是否小于ReadView中的min_trx_id。会发现txr_id=32,是小于事务A的ReadView里的min_trx_id = 45的。这说明事务A开启前,修改这行数据的事务已经提交了。所以此时可以查到trx_id = 32这一行数据,如下图示:

时间点4:

接下来事务B开始操作该条数据。它把这行数据的值修改为值B,将这行数据的txr_id设置为自己的ID即59。同时roll_pointer指向修改前事务B生成的一个undo log,然后事务B提交,如下图示:

时间点5:

这时事务A再次执行查询,但是却发现数据行里的txr_id=59。此时这个txr_id=59是大于ReadView里的min_trx_id(45),同时小于ReadView里的max_trx_id = 60的。说明更新这条数据的事务,很可能就跟自己差不多同时开启的。于是会看一下这个txr_id=59,是否在ReadView的m_ids列表里。

然后在ReadView的m_ids列表里,发现有45和59两个事务ID。这就证实了这个修改数据的事务是跟自己同一时段并发执行然后提交的。所以对trx_id=59这一行数据是不能查询的,如下图示:

时间点6:

通过上面的判断,事务A就知道这条数据不是它改的,不能查。所以要根据roll_pointer顺着undo log版本链向下找。然后回找到最近的一条undo log,它的trx_id是32。此时发现trx_id=32是小于ReadView里的min_trx_id = 45的,说明这个undo log版本必然是在事务A开启前就执行且提交的,那么就查询这个undo log的值即可。

于是undo log版本链的作用就体现出来了,undo log版本链相当于保存了一条快照链条。事务A读取到的数据,就是之前的快照数据。

三.Read View的更新方式

接下来分析RC级别和RR级别,因为MVCC不适用于其它两个隔离级别。

四.Read Committed级别

当数据库处于RC隔离级别时,每次执行select都会创建新的Read View,从而保证每次select都能读取到其他事务已经提交的内容。

读已提交的语义就是防止脏读,比如下面是一个通过每次查询创建Read View实现读已提交的例子。

时间点1:

假设当前有事务A(id=60)与事务B(id=70)并发执行。在当前级别下,事务A每次select时都会创建新的Read View。首先事务B修改这条数据,trx_id被设置为70,同时会生成一条undo log由roll_pointer来指向,然后事务A发起一次查询操作生成一个Read View,如下图示:

时间点2:

事务A发起查询时,发现当前这条数据的trx_id=70。根据Read View机制,trx_id=70在min_trx_id与max_trx_id的范围之间,而且trx_id=70存在于m_ids=[60,70]数组中,这表示的是事务B是和事务A同一时刻开启的事务,并且还没提交。所以事务A无法查询到事务B修改的值。

于是事务A会顺着undo log版本链往下查找,发现trx_id=50。根据Read View机制,trx_id=50小于min_trx_id,表示在当前事务开启前,trx_id=50这个事务就已操作该数据并提交了,所以当前事务能读到这个快照数据。

时间点3:

接着事务B提交了。事务B一旦提交,那么事务A下次再查询时,就能读到事务B修改过的值。因为在事务A新生成的Read View的m_ids里已移除trx_id=70,可以读了。

如何保证事务A能读取已提交的事务B的数据呢?其实很简单,就是事务A再次查询时生成一个新的Read View。注意此时新的Read View的min_trx_id还是没提交的事务A的trx_id=60,如下图示:

以上就是基于Read View实现RC隔离级别的原理,其本质是协调多个事务并发读写同一批数据时应如何协调互相的可见性。

五.Repeatable Read级别

第一次查询时生成的Read View不会再更新,后续所有查询都复用它。所以能保证每次读取的一致性,也就是都可以读取第一次读取到的内容。可重复读的语义就是防止幻读。

时间点1:

假设有一条数据是trx_id=50的事务插入的,同时有事务A(id=60),事务B(id=70)。此时事务A发起了一个查询,在RR级别下第一次查询会生成Read View。根据规则事务A可以查询到数据,因为数据的trx_id=50小于min_trx_id。

时间点2:

接着事务B进行操作并提交事务,事务B的trx_id=70。因为是RR级别,所以Read View一旦生成就不会改变了。虽然事务B提交了并结束了,但是事务A的Read View的m_ids中仍然还是[60,70]两个事务ID。

事务A进行第二次查询发现数据的trx_id=70,70在min_trx_id与max_trx_id之间,同时又在m_ids数组中。表示事务A开启查询时,trx_id=70的事务B当时正在运行。因此事务A是不能查询到事务B更新的这个值的,于是事务A还要顺着指针往历史版本链条上去找。

接着找到trx_id=50这个版本的数据,50小于min_trx_id=60,说明开启前已提交,可以查询到。

时间点3:

至此事务A多次读同一个数据,每次读到的都是一样的值。除非是它事务A自己修改了值,否则读到的一直会是一样的值。

以上就是RR级别的实现原理。

六.Read View总结

通过Read View判断记录的某个版本是否可见总结:

情形1:trx_id = creator_trx_id

如果被访问版本的trx_id与Read View中的creator_trx_id值相同,那么表明当前事务在访问自己修改过的记录,该版本可被当前事务访问。

情形2:trx_id < min_trx_id

如果被访问版本的trx_id小于Read View中的min_trx_id值,那么表明生成该版本的事务在当前事务生成Read View前已经提交,所以该版本可以被当前事务访问。

情形3:trx_id >= max_trx_id

如果被访问版本的trx_id大于或等于Read View中的max_trx_id值,那么表明生成该版本的事务在当前事务生成Read View之后才开启,所以该版本不可以被当前事务访问。

情形4:trx_id > min_trx_id && trx_id < max_trx_id

如果被访问版本的trx_id值在Read View的min_trx_id和max_trx_id之间,那么就需要判断trx_id属性值是不是在m_ids列表中。如果在,则说明创建Read View时生成该版本的事务还是活跃的,于是这个被访问的版本的数据值不可被访问。如果不在,则说明创建Read View时生成该版本的事务已被提交,于是这个被访问的版本的数据值可被访问。

七.生成Read View时机

RC隔离级别:

每次读取数据前,都生成一个新的Read View。

RR隔离级别:

在第一次读取数据前,生成一个Read View,之后Read View不再更新。

(4)MVCC在MySQL中的具体实现

MySQL实现MVCC机制的方式:undo log多版本链 + Read View机制。在MySQL中实现MVCC时,会为每一个行记录添加如下几个隐藏的字段。

字段一:6字节的DATA_TRX_ID

DATA_TRX_ID标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动设置为当前事务ID,DATA_TRX_ID只有在事务提交之后才会更新。

字段二:7字节的DATA_ROLL_PTR

一个rollback指针,指向当前这一行数据的上一个版本。通过这个指针可以找到之前版本的数据,这个指针会将数据的多个版本连接在一起构成一个undo log版本链。

字段三:6字节的DB_ROW_ID

隐含的自增ID,这是一个用来唯一标识每一行的字段。如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。

字段四:DELETE BIT位

标识当前记录是否被删除,不是真正的删除数据,而是标志出来要删除,真正意义的删除是在commit时执行的。

(5)SELECT、DELETE、 INSERT、 UPDATE语句对隐藏字段的处理

一.查询SELECT

条件一:只查找版本早于当前事务版本的数据行

也就是数据行的版本必须小于等于事务的版本,这确保当前事务读取的行都是事务之前已经存在的,或者当前事务读取的行是由当前事务创建或修改的行。

条件二:行的删除操作的版本一定是未定义的或大于当前事务的版本号

确定当前事务开始前行没有被删除。

符合以上两个条件才返回查询结果。

二.删除DELETE

修改DATA_TRX_ID的值为当前执行删除操作的事务的ID,然后设置DELETE BIT为true表示被删除。

三.增加INSERT

设置新记录的DATA_TRX_ID为当前事务ID,其他的采用默认的。

四.修改UPDATE

用排它锁锁定该行,因为是写操作。记录redo log:将更新之后的数据记录到redo log中,以便日后使用。记录undo log:将更新之前的数据记录到undo log中。

(6)MVCC读操作分类

在MVCC并发控制中,读操作可以分为两类:快照读(Snapshot Read)与当前读(Current Read)。

一.快照读

指读取数据时不读取最新版本的数据,而是基于历史版本读取快照信息,比如InnoDB会读取undo log历史版本。快照读可以使普通的SELECT读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题。

问题1:因加锁导致的修改数据时无法对数据进行读取的问题。

问题2:因加锁导致的读取数据时无法对数据进行修改的问题。

二.当前读

当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的。比如以下语句就是当前读:update、delete、insert、select ... lock in share mode、select for update。

MVCC已经实现了读读、读写、写读的并发冲突,如果想进一步解决写写冲突,可采用这两种方案:乐观锁、悲观锁。

10.事务隔离级别之隔离级别的类型

通过设置隔离级别,可防止上面的三种并发问题。MySQL有四种隔离级别,上面级别最低,下面级别最高。Y表示会出现问题,N表示不会出现问题。

一.Read Uncommitted读未提交

解决了回滚覆盖类型的更新丢失,但可能发生脏读现象,一个事务可能读取到另一个事务修改但是没有提交的数据。

二.Read Committed读已提交

只能读取到其他会话中已经提交的数据,解决了脏读,但可能发生不可重复读现象 + 幻读,也就是可能在一个事务中两次查询结果不一致。

三.Repeatable Read可重复读

解决了不可重复读,它确保同一事务的多次读取一行数据时,会看到同样的数据行。不过理论上会出现幻读,因为幻读指的是:当用户读取某一范围的数据行时,另一事务又在该范围插入了新的数据。当用户在读取该范围的数据时,会发现有新的幻影行。

四.可串行化

所有的增删改查串行执行,它通过强制事务排序,解决相互冲突,从而解决幻读的问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。

注意:数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差。读未提交隔离级别最低,并发问题多,但并发处理能力好。以后使用时,可根据系统特点来选择一个合适的隔离级别。比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力时,可使用Read Commited隔离级别。

11.事务隔离级别之和锁的关系

一.事务隔离级别是SQL92定制的标准,是事务并发控制的整体解决方案。事务隔离级别本质上是对锁和MVCC使用的封装,隐藏了底层细节。

二.锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现。对相应操作加不同的锁,就可以防止其他事务同时对数据进行读写操作。

三.对用户来讲,首先选择使用隔离级别。当选用的隔离级别不能解决并发问题时,才有必要在开发中手动设置锁。MySQL默认隔离级别是Repeatable Read。Oracle和SQLServer默认的隔离级别是Read Committed。

12.事务隔离级别之MySQL隔离级别控制

MySQL默认的事务隔离级别是Repeatable Read,查看当前的事务隔离级别命令如下:

mysql> show variables like 'tx_isolation';
mysql> select @@tx_isolation;

设置事务隔离级别可以如下命令:

set tx_isolation='READ-UNCOMMITTED';
set tx_isolation='READ-COMMITTED';
set tx_isolation='REPEATABLE-READ';
set tx_isolation='SERIALIZABLE';

#牛客创作赏金赛#
MySQL底层原理与应用 文章被收录于专栏

MySQL底层原理与应用

全部评论

相关推荐

11-16 11:50
已编辑
门头沟学院 golang
点赞 评论 收藏
分享
评论
1
1
分享
牛客网
牛客企业服务