天天看点

MySQL数据库的事务控制演进

作者:程序员阿龙

一、排队

排队处理是事务管理最简单的方法,就是完全顺序执行所有事务的数据库操作,不需要加锁,简单地说就是全局排队。序列化执行所有的事务单元,数据库某个时刻只处理一个事务操作,特点是强一致性,处理性能低。

MySQL数据库的事务控制演进

二、排它锁

引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。

MySQL数据库的事务控制演进

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

三、读写锁

读和写操作:读读、写写、读写、写读。

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

MySQL数据库的事务控制演进

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

四、MVCC

1、MVCC概念

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

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

2、Undo log 多版本链

在介绍MVCC之前,我们首先来了解一下Undo Log多版本链,了解了这个机制才能更好的理解MVCC机制。

每条数据其实都有两个隐藏字段,事务ID(trx_id) ,回滚指针(roll_pointer)

  • trx_id: 记录最近一次更新这条数据的事务id
  • roll_pointer: 指向之前生成的 undo log

通过下面的例子,我们来一起看一下版本链的生成:

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

MySQL数据库的事务控制演进

2. 接着又有一个事务B (trx_id=58) 过来,对同一条数据进行修改,将值改为B,事务B的id是58,在更新之前会生成一个undo log来记录之前的值.然后会让roll_pointer指向这个实际的undo log回滚日志:

MySQL数据库的事务控制演进

3. 如果再有一个事务C (trx_id=69) 继续更新该条记录值为C,则会跟第二步的步骤一样,

MySQL数据库的事务控制演进

总结: 每一条数据都有多个版本,版本之间通过undo log链条进行连接

通过这样的设计,可以保证每个事务提交的时候一旦需要回滚操作,可以保证同一个事务只能读到比当前版本更早提交的值,不能看到更晚提交的值。

3、ReadView

刚才讲解了undo log版本链,接下来说一下基于undo log版本链实现的Read View视图机制.

什么是Read View?

Read View是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。

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所存储的信息:

MySQL数据库的事务控制演进

我们一起来看下面的这个例子:

1. 假设数据库有一行数据,很早就有事务操作过,事务id 是32. 此时两个事务并发过来执行了, 一个事务A (id=45),一个事务B (id=59). 事务A需要读取数据,而事务B需要更新数据,如下图:

MySQL数据库的事务控制演进

如果不加任何限制,这里会出现脏读的情况,也就是一个事务可能会读到一个没有提交的值。

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

MySQL数据库的事务控制演进

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

MySQL数据库的事务控制演进

4. 接下来事务B开始操作该条数据,他把这行数据的值修改为了值B,然后将这行数据的 txr_id 设置为自己的id,也就是59,同时 roll_pointer 指向了修改之前生成的一个undo log,然后事务B提交,如下图所示:

MySQL数据库的事务控制演进

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

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

MySQL数据库的事务控制演进

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

那么就查询最近的那个undo log里的值就可以了,这时undo log版本链的作用就体现出来了,它相当于保存了一条快照链条,而事务A读取到的数据,就是之前的快照数据。

MySQL数据库的事务控制演进

Read View的更新方式:

我们只分析RC级别和RR级别,因为MVCC不适用于其它两个隔离级别。

Read Committed级别

当你一个事务设置他处于RC隔离级别时, 每次执行select都会创建新的read_view,更新旧read_view,保证能读取到其他事务已经COMMIT的内容(读提交的语义)

比如下面这个例子:

1. 假设当前有事务A(id=60)与事务B(id=70) 并发执行,在当前级别下,事务A每次select的时候都会创建新的read view。

首先事务B修改这条数据, trx_id被设置为70,同时会生成一条undo log,由roll_point来指向.然后事务A发起一次查询操作,生成一个read view,如下图:

MySQL数据库的事务控制演进

2. 接着事务A会发起查询,发现当前这条数据的trx_id=70

根据read view机制的规则: 范围是在min_trx_id与max_trx_id的范围之间,并且trx_id存在于m_ids数组中,trx_id在m_ids数组中表示的是事务B是和事务A同一时刻开启的事务,并且还没有提交. 事务A是无法查询到事务B修改的值的。

事务A会顺着undo log版本链往下查找,发现原始值,trx=50

根据read view机制的规则: 当前数据的trx_id小于min_trx_id,表示这条数据是在当前事务开启之前,其他事务就已经操作了该条数据,并且提交了事务,所以当前事务能读到这个快照数据。

MySQL数据库的事务控制演进

3. 接着事务B提交了,事务B一旦提交,那么事务A下次再查询的时候,就可以读到事务B修改过的值了。如何保证事务A可以读取到已经提交的事务B的数据呢? 其实很简单,就是事务A再次查询时 会生成一个新的read view,如下图:

MySQL数据库的事务控制演进

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

Repeatab Read级别

第一次select时更新这个read_view,以后不会再更新,后续所有的select都是复用这个read_view。所以能保证每次读取的一致性,即都是读取第一次读取到的内容(可重复读的语义)。

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

MySQL数据库的事务控制演进

2. 接着事务B进行操作,并提交事务,事务B的trx_id=70.因为是RR级别,所以Read View一旦生成就不会改变了,所以事务B虽然结束了,但是事务A的Read View的m_ids中仍然还是[60,70]两个事务id事务进行第二次查询发现数据的trx_id=70, 70在min_trx_id与max_trx_id之间,同时又在m_ids数组中,这表示的是事务A开启查询的时候,事务B当时正在运行.因此事务A是不能查询到事务B更新的这个值的,因此还要顺着指针往历史版本链条上去找。

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

MySQL数据库的事务控制演进

3. 至此事务A多次读同一个数据,每次读到的都是一样的值,除非是他自己修改了值,否则读到的一直会一样的值,这就是RR级别的实现原理。

Read View总结:

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

trx_id = creator_trx_id

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

trx_id < min_trx_id

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

trx_id >= max_trx_id

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

trx_id > min_trx_id && trx_id < max_trx_id

如果被访问版本的trx_id,值在readview的min_trx_id和max_trx_id之间,就需要判断trx_id属性值是不是在m_ids列表中?

  • 在:说明创建readview时生成该版本的事务还是活跃的,该版本不可以被访问
  • 不在:说明创建readview时生成该版本的事务已经被提交,该版本可以被访问

生成readview时机

  • RC隔离级别:每次读取数据前,都生成一个readview。
  • RR隔离级别:在第一次读取数据前,生成一个readview,之后read view不再更新。

4、MVCC在mysql中的具体实现

MySQL实现MVCC机制的方式: undo log多版本链+ReadView机制。

在MySQL中 实现MVCC时,会为每一个表添加如下几个隐藏的字段:

  • 6字节的 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的时候。

下面分别以select、delete、 insert、 update语句来说明:

1、查询 SELECT

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

2.行的删除操作的版本一定是未定义的或者大于当前事务的版本号,确定了当前事务开始之前,行没有被删除。

符合了以上两点则返回查询结果。

2、删除 DELETE

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

3、增加:INSERT

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

4、修改:UPDATE

用排它锁锁定该行(因为是写操作)

记录redo log:将更新之后的数据记录到redo log中,以便日后使用。

记录undo log:将更新之前的数据记录到undo log中。

5、MVCC读操作分类

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

1、快照读

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

  1. 解决了因加锁导致的修改数据时无法对数据读取问题。
  2. 解决了因加锁导致读取数据时无法对数据进行修改的问题。

2、当前读

当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的( Update delete insert select ....lock in share mode , select for update 为当前读)

MVCC已经实现了读读、读写、写读并发处理,如果想进一步解决写写冲突,可以采用下面两种方案:

  • 乐观锁
  • 悲观锁

继续阅读