一、事务是什么
事务是一系列数据库操作的集合,这些操作全部成功即为事务成功,有一个失败即为 事务失败,所有操作全部回滚。
二、事务的特性 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_id 、 roll_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都是同一个,所以查询的结果也相同,实现可重复读。