Mysql事务 隔离级别、并发问题、MVCC原理

361 阅读6分钟

一、事务是什么

事务是一系列数据库操作的集合,这些操作全部成功即为事务成功,有一个失败即为 事务失败,所有操作全部回滚。

二、事务的特性 ACID

2.1 原子性

事务是最小操作单元,要么全部成功,要么全部失败,不存在只成功一部分的情况。

2.2 一致性

一致性是针对现实业务场景所展现出的特性,例如一个转账的操作,A余额扣除200,b余额则会增加200,不会出现A扣钱B没有增加的情况。

2.3 隔离性

一个事务的执行过程不受到另一个事务的干扰,即两个事务互相隔离。(隔离级别在后续会提到)

2.4 持久性

事务提交后,对于数据库的修改是永久生效的。

三、隔离级别和并发问题

多个事务在并发执行过程中,存在多种并发问题,包括脏写、脏读、不可重复读、幻读,这几种问题严重性依次递增。

脏写:一个事务中修改的数据被另一个事务回滚掉。 在mysql的innodb引擎中,默认的排他锁解决了脏写问题。

脏读:一个事务中读取到了另一个事务未提交的数据。

不可重复度:一个事务中多次重复读取一条数据,得到的结果不一样(其他事务在两次读取的间隙做了修改提交,导致该事务读取到了其他事务修改的数据)。

幻读:在一个事务中进行范围查询,第二次读取到的数据条数增加。(和不可重复度类似,但幻读的关键在于数据条数的增加

四、隔离级别

4.1 读未提交 Read Uncommitted

仅限制脏写问题。

表现:一个事务会读取到另一个事务未提交的数据,即脏读。

存在:脏读、不可重复读、幻读。

4.2 读已提交 Read Committed

解决脏写问题、脏读问题。

表现:一个事务中只能读取到另一个事务已经提交过的数据。

存在:不可重复读、幻读。

4.3 可重复读 Repeatable Read

解决脏写、脏读、不可重复读问题。

表现:一个事务中不会读取到其他事务提交的修改,即每次读取到的数据都是一样的,可重复读。

存在:幻读。RR+间隙锁可以解决幻读问题。

4.4 串行化 Serializable

解决所有并发问题,通过加锁实现,强制所有事务串行化执行。

五、MVCC实现原理

Mysql如何保证事务的隔离性,一是加锁,二是通过MVCC,即多版本并发控制。

加锁是一种简单粗暴的解决并发问题的办法,但存在一定的弊端,即影响并发性能。

在RC和RR隔离级别中,Mysql都是采用MVCC来处理读写冲突,实现事务隔离性,并提高数据库的并发性能。

5.1 事务ID和innodb隐式字段

每一个事务都存在一个自增的id,此为该事务的唯一标识。

在innodb引擎的数据表中,每一行数据都存在三个隐式字段,trx_idroll_pointer,分别为 最后修改该条数据的事务id 和 该数据的回滚指针。

roll_pointer指针 指向undo log中的一条历史记录,即该条数据上次修改前备份的记录。

第三个隐式字段row_id,是在自增主键不存在的时候,mysql自动生成的隐式主键。

5.2 undo log 回滚日志

在进行update和delete操作时,会生成一条undo log回滚日志,备份修改前的数据,用于事务回滚操作。

Innodb通过undo log保存了已更改行的旧版本的信息的快照。

undo log中为每一行数据增加了三个隐藏列

DB_TRX_ID 插入、更新、删除该行的最后一个事务的ID

DB_ROLL_PTR 回滚指针,指向上一条log记录

DB_ROW_ID 该行自增ID

DB_ROLL_PTR的连接下,undo log呈现一个链表形式,也就是该行数据的版本链。

undo log 在MVCC中的作用:

MVCC的实现,主要利用了undo log中的 DB_ROW_ID ,判断该版本的数据对于某一事务来说是否可见。

但我们并不清楚 DB_ROW_ID 所指向的事务的状态,接下来我们引入read view,在read view中,记录了当前未提交的事务合集等一系列信息,用于判断undo log中的版本数据的可见性。

5.3 read view 读视图,用于判断此次查询数据的可见版本

事务中,在第一次select之前,会生成一个read view 读视图。

read view 中包含以下字段:

m_ids —— read view 生成时,所有未提交的事务id集合

min_limit_id —— m_ids中最小的事务id

max_limit_id —— m_ids中最大数据加一

creator_trx_id —— 生成 read view 视图的事务id

那么是如何通过read view 和 undo log 判断数据是否可见呢?

首先我们从undo log中取出最新的记录,用 DB_TRX_ID 和 read view 的 m_ids 比较,会有以下几种情况:

1、DB_TRX_ID 在 m_ids 合集中

如果 DB_TRX_ID 在 m_ids 合集中,且那么表示该版本的数据处于未提交状态,不可见。

如果 DB_TRX_ID 在 m_ids 合集中,且 DB_TRX_ID 等于 creator_trx_id,可见。(该条数据是由当前事务生成的。)

2、DB_TRX_ID 在 m_ids 合集外左侧

DB_TRX_ID 小于 min_limit_id (m_ids中的最小事务id),因为事务id都是自增,小于 min_limit_id ,则表示 DB_TRX_ID 事务处于提交状态,可见。

3、DB_TRX_ID 在 m_ids 合集外右侧

DB_TRX_ID 大于等于 max_limit_id (m_ids中最大事务id+1),大于等于 max_limit_id 的事务,都是在read view生成后创建的,不可见。

4、总结

只有 当前事务生成的数据在read view生成之前提交的事务生成的数据,才符合可见标准。

如果第一条数据不可见:

需要利用 undo log 中的回滚指针 DB_ROLL_PTR ,寻找上一个版本的记录,再次进行判断。

依次类推,直到寻找到第一个可见记录。

六、可重复读 RR 和读已提交 RC

RR 和 RC 两种隔离级别都是根据 MVCC 原理来实现,两者之间的区别便是可重复读与不可重复读。

这种区别到底是如何出现的呢?其实很简单:

两种隔离级别不同的关键,就在于 read view 的生成时机。

在 RC 隔离级别中,read view 会在每次进行 select 之前生成;

而 RR 隔离级别,read view 只会在首次 select 前生成一次,后续不再重复生成。

在 read view 生成时,会记录所有未提交的事务合集,RC 的每次重新生成read view ,会把所有已提交的事务id排除出m_ids,所以已提交的事务生成的数据属于可见范围, 其他事务有新提交的数据,也会查询出来,不可重复读。

在 RR 中,read view 只会在首次 select 生成,也就是事务中每次 select 所用来判断的read view中的m_ids都是同一个,所以查询的结果也相同,实现可重复读。