这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战
一、事务隔离级别
如果在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据,这样处理对性能影响大,即想保持事务的隔离性,又想服务器在处理访问同一个数据的多个事务时性能高,舍一部分隔离而取性能。
事务并发执行遇到的问题
访问相同数据的事务在不保证串行执行的情况下可能会出现的问题:\
- 脏写(Dirty write):如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写
image.png
Session A 和 Session B 各开启一个事务,Session B中的事务先更新,然后Session A中的事务接着更新并提交事务,如果之后Session B中的事务进行了回滚,那么Session A 中的更新也将不复存在,这种现象就称之为脏写。
\
- 脏读(Dirty Read):如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读
image.png
Session A 和 Session B各开启了一个事务,Session B中的事务先将列更新为xxx,然后Session A中的事务再去查询这条记录,如果读到列值为xxx,而后Session B中的事务进行回滚,那么Session A中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读。
\
- 不可重复读(Non-Repeatable Read):如果一个事物只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读。
image.png
在session B 中提交了几个隐式事务(语句结束事务就提交),这些事务都修改了列值,每次事务提交之后,如果Session A中的事务都可以查看到最新值,这种现象也被之为不可重复读。
\
- 幻读(Phantom): 如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读。
\
image.png
Session A中的事务先根据条件id>0查询表,得到了列值xxx的记录,之后Session B中提交了一个隐式事务,该事务向表中插入了一条新记录;之后Session A中的事务再根据相同的条件查询表,得到的结果集中包含Session B中的事务新插入的记录,那这种现象也被称之为幻读。
幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录,对于先前已经读到的记录,之后又读取不到这种情况,其实这相当于对每一条记录都发生了不可重复读的现象。
二、SQL标准中的四种隔离级别
并发事务执行过程中的问题,按照严重性来排一下序如下 :
脏写 > 脏读 > 不可重复读 > 幻读
制定SQL标准,在标准中设立了4个隔离级别:
- READ UNCOMMITTED:未提交读
- READ COMMITTED:已提交读
- REPEATABLE READ:可重复读
- SERIALIZABLE:可串行化
image.png
- READ UNCOMMITTED 隔离级别下,可能发生脏读、不可重复读和幻读问题
- READ COMMITTED 隔离级别下,可能发生不可重复读和幻读问题,但是不可以发生脏读问题
- REPEATABLE READ 隔离级别下,可能发生幻读问题,但是不可以发生脏读和不可重复读的问题
- SERIALIZABLE 隔离级别下,各种问题都不可能发生。
- 无论是那种隔离级别,都不允许脏写的情况发生。
三、MVCC原理(multi-version concurrency control 多版本并发控制)
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列
- trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务ID赋值给trx_id隐藏列
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本号写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息
假设插入该记录的事务id=80,那么此刻该条记录如下所示:\
假设之后两个事务分别为100、200的事务对这条记录进行UPDATE操作,操作流程如下:\
image.png
InnoDB使用锁来保证不会有脏写情况的发生,也就是在第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表如下:\
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,把这个链表称之为版本链,版本链的头节点就是当前记录最新的值,另外,每个版本中还包含生成该版本时对应的事务id
四、ReadView
对于使用Read Uncommited 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;
对于使用Serializable隔离级别的事务来说,使用加锁的方式来访问记录;
对于使用Read Commited 和 Repeatable Read 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的,为此提出ReadView的概念,这个ReadView中主要包含4个比较重要的内容:
\
-
m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务ID列表
-
min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务ID,也就是m_ids中的最小值
-
max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的ID值
注意max_trx_id并不是m_ids中的最大值 ,事务id是递增分配的。比如现有id为1,2,3这三个事务,之后id为3的事务提交了,那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4
-
creator_trx_id:表示 生成该ReadView的事务的事务id
只有在对表中的记录做改动时(执行Insert、Delete、Update时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
有了ReadView,在访问某条记录时,需要按照下边的步骤判断记录的某个版本是否可见:
\
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问
- 如果被访问版本的trx_id属性值小于ReadView中min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当事务访问。
- 如果访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
在Mysql中 Read commited 和Repeatable Read隔离级别的一个非常大的区别在于生成ReadView的时机不同。
READ COMMITTED ---每次读取数据前都生成一个ReadView
系统里有两个事务id分别为100、200的事务在执行
# Transaction 100
BEGIN;
UPDATE table SET name = 'xx1' WHERE id = 1;
UPDATE table SET name = 'xx2' WHERE id = 1;
# Transaction 200
BEGIN;
UPDATE table SET name = 'xx3' WHERE id = 1;
UPDATE table SET name = 'xx4' WHERE id = 1;
如果现在有一使用READ COMMITTED隔离级别的事务开始执行:
1)Transaction 100、200未提交
# SELECT1:Transaction 100、200未提交
SELECT * FROM table WHERE id = 1; # 得到的列name的值为'xxx'
Select1的执行过程如下:
- 在执行select语句时会生成一个ReadView,ReadView的m_ids列表的内容就是【100,200】,min_trx_id为100,max_trx_id为201,creator_trx_id为0
- 然后从版本链中挑选可见的记录,最新版本的列name的内容是xx2,该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
- 下一个版本的列name的内容是xx1,该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本
- 下一个版本的列name的内容是xxx,该版本的trx_id值为80,小于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给用户的版本就是这列name=xxx的记录
2)把事务id=100的事务提交
# SELECT2:Transaction 100 提交,Transaction 200未提交
SELECT * FROM table WHERE id = 1; # 得到的列name的值为'xx2'
Select2的执行过程如下:
- 在执行select语句时又会生成一个ReadView,该ReadView的m_ids列表的内容就是【200】(事务id=100的事务已经提交,所以再次生成快照时就没有它),min_trx_id为200,max_trx_id为201,creator_trx_id为0
- 然后从版本链中挑选可见的记录,最新版本的列name的内容是xx4,该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本
- 下一个版本的列name的内容为xx3,该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本
- 下一个版本的列name内容为xx2,该版本的trx_id值为100,小于ReadView中的min_trx_id值200,所以这个版本是符合要求的,最后返回级用户的版本就是这条记录的name为xx2
以此类推,如果事务id=200也提交了,再次查询表id=1值的记录时,得到的结果就是xx4。
REPEATABLE READ---在第一次读取数据时生成一个ReadView
# Transaction 100
BEGIN;
UPDATE table SET name = 'xx1' WHERE id = 1;
UPDATE table SET name = 'xx2' WHERE id = 1;
# Transaction 200
BEGIN;
UPDATE table SET name = 'xx3' WHERE id = 1;
UPDATE table SET name = 'xx4' WHERE id = 1;
如果现在有一使用REPEATABLE READ隔离级别的事务开始执行:
1)Transaction 100、200未提交
# SELECT1:Transaction 100、200未提交
SELECT * FROM table WHERE id = 1; # 得到的列name的值为'xxx'
Select1的执行过程如下:
- 在执行select语句时会生成一个ReadView,ReadView的m_ids列表的内容就是【100,200】,min_trx_id为100,max_trx_id为201,creator_trx_id为0
- 然后从版本链中挑选可见的记录,最新版本的列name的内容是xx2,该版本的trx_id值为100,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本。
- 下一个版本的列name的内容是xx1,该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本
- 下一个版本的列name的内容是xxx,该版本的trx_id值为80,小于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给用户的版本就是这列name=xxx的记录
2)把事务id=100的事务提交
# SELECT2:Transaction 100 提交,Transaction 200未提交
SELECT * FROM table WHERE id = 1; # 得到的列name的值为'xxx'
Select2的执行过程如下:
- 当前事务的隔离级别为REPEATABLE READ,而之前在执行select1时已经生成过ReadView了,所以此时直接复用之前的ReadView,之前的ReadView的m_ids列表的内容就是【100,200】,min_trx_id为100,max_trx_id为201,creator_trx_id为0
- 然后从版本链表挑选可见的记录,最新版本的列name的内容是xx4,该版本的trx_id值为200,在m_ids列表内,所以不符合可见性要求,根据roll_pointer跳到下一个版本
- 下一个版本的列name的内容是xx3,该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下一个版本
- 同理下一个列的版本也不符合要求,继续跳到下一个版本
- 下一个版本的列name的内容是xxx,该版本的trx_id值为80,小于Read中的min_trx_id值100,所以这个版本是符合要求的。最后返回列值xxx的记录
五、总结
MVCC指的就是在使用Read Committed、Repeatable read这两种隔离级别的事务在执行普通的select操作时访问记录的版本链的过程,可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
Read Committed、Repeatable read这两个隔离级别的不同就是:生成ReadView的时机不同,Read Committed在每一次进行普通select操作前都会生成一个ReadView,而Repeatable Read只在第一次进行普通select操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView.