MySQL事务隔离级别及MVCC详解

MySQL的事务隔离级别及特性如下:

SERIALIZABLE(串行化):无问题,性能差;

REPEATABLE-READ(可重复读):幻读,默认隔离级别;

READ-COMMITTED(已提交读):不可重复读&幻读;

READ-UNCOMMITTED(未提交读):脏读&不可重复读&幻读。

串行化就是事务按顺序执行,没有并行,自然也就没有问题。但是性能就会比较差;

可重复读是MySQL默认的隔离级别,就是一个事务中多次读取数据,读到的数据都是一致的,不受其他事务修改的影响;

已提交读就是可以读到其他事务已经提交的数据,所以就没有了可重复读的特性;

未提交读能读到其他事务已执行但是未提交的数据,这就会产生脏读。

数据库事务隔离级别的查看和修改

sql 复制代码
-- 查看(5.7)
select @@global.tx_isolation;
select @@tx_isolation;
-- 查看(8)
select @@global.transaction_isolation;
select @@transaction_isolation;
-- 修改
set session transaction isolation level serialiable/repeatable read/......

可重复读出现幻读的情况

A事务:

sql 复制代码
start transaction;
select * from table where id = 2; //此时表中无id为2的数据

B事务:

sql 复制代码
start transaction;
insert into table(id, name) values(2, 'Wang');
commit;

A事务:

sql 复制代码
select * from table where id = 2; // 此时虽然表中已经有了,但是由于可重复读的特性,同一事务读到的数据是一致的,所以还是读不到
insert into table(id, name) values(2, 'Wang'); //报错
commit;

已提交读出现不可重复读的情况

A事务:

sql 复制代码
start transaction;
select * from table where id = 2; // 此时可以看到2的name是'Wang'

B事务:

sql 复制代码
start transaction;
update table set name = 'Yan' where id = 2;
commit;

A事务:

sql 复制代码
select * from table where id = 2; // 此时2的name变成了'Yan',这样就出现了不可重复度的问题
commit;

已提交读出现幻读的情况

A事务:

sql 复制代码
start transaction;
select * from table where id = 3; // 此时没有id为3的数据,

B事务:

sql 复制代码
start transaction;
insert into table(id, name) values(3, 'Li'); // 只插入不提交

A事务:

sql 复制代码
select * from table where id = 3; // 此时还是读不到3的数据
insert into table(id, name) values(3, 'Li'); // 此时插入数据就会有问题,因为B事务还没提交,但是已经执行了,A事务只能等待,看B事务是回滚还是提交

未提交读出现不可重复读和脏读的情况

A事务:

sql 复制代码
start transaction;
select * from table where id = 2; // 此时读到2的name为'Yan'

B事务:

sql 复制代码
start transaction;
update table set name = 'Sun' where id = 2; // 只执行不提交

A事务:

sql 复制代码
select * from table where id = 2; // 此时读到2的name为'Sun',这就已经出现了不可重复读。假设B事务现在回滚了,那么这个数据就是错的,就出现了脏读。

为提交读出现幻读的情况

A事务:

sql 复制代码
start transaction;
select * from table; // 查出全表数据为[1-'Wu'], [2-'Sun'], [3-'Li']

B事务:

sql 复制代码
delete from table where id = 3; // 删除了但是没提交

A事务:

sql 复制代码
select * from table; // 查出全表数据为[1-'Wu'], [2-'Sun'],查到3被删除了。此时B事务回滚,数据3又出来了,一会有一会没有,就出现了幻读。

MVCC

MVCC即多版本并发控制(Multi-Version Concurrent Control)。

使用可重复读和已提交读这两种隔离级别时,SELECT操作版本链的过程。

MySQL数据行中存在隐藏的3个列:row_id、trx_id、roll_pointer

row_id如果表中有主键,或者有非NULL唯一键,就不需要row_id。没有则会自动生成6个字节的row_id。

trx_id为事务id,每次开启事务都会生成,记录在数据行中。

roll_pointer则是会指向undo_log中的上一次更新的数据。更新多次就会形成版本链。

SELECT操作会生成ReadView,已提交读同一个事务中对相同数据的查询每次SELECT都会生成一个,而可重复读一个事务多个SELECT只会生成一个ReadView。

ReadView中有m_ids、min_trx_id、max_trx_id、creator_trx_id。

m_ids:生成ReadView时所有未提交的事务id。

min_trx_id:m_ids的最小值。

max_trx_id:生成ReadView时,系统应该分配给下一个事务的事务id。

creator_trx_id:生成ReadView的事务的事务id,如果只有SELECT则为0。

当SELECT生成ReadView后,从当前B+树中的数据开始查,顺着版本链从上往下查,知道找到ReadView生成前提交的最新记录,返回即可。有以下几种情况:

  1. 被访问数据的事务id小于min_trx_id,返回。
  2. 被访问数据的事务id大于等于max_trx_id,不返回。
  3. 被访问数据的事务id在m_ids中,不返回。
  4. 被访问数据的事务id在min_trx_id和max_trx_id之间,且不在m_ids中,返回。

已提交读举例

有数据1-Wang, 2-Yan, 3-Li,插入事务id均为80。

A事务id为100更新1 = Sun,不提交。

B事务id为200更新其他表,不提交。

C事务读id为1的数据,生成ReadView,m_ids=100, 200,min_trx_id=100, max_trx_id=201,creator_trx_id=0。

这时就该从版本链中开始寻找数据:

  1. id为1的数据目前的trx_id为100,100在m_ids中,说明那个事务在当前事务开始的时候还没执行完,不应该读出来,继续向下寻找。
  2. 此时roll_pointer指向的上次undo_log记录trx_id为80,80小于min_trx_id,说明这个事务在生成ReadView之前已经提交了,那就会读到'Wang'。

A事务提交。

B事务更新1 = Liu,不提交。

C事务读id为1的数据,再次生成ReadView,m_ids=200,min_trx_id=200,max_trx_id=201,creator_trx_id=0。

从版本链中开始寻找数据:

  1. id为1的数据目前的trx_id为200,200在m_ids中,继续向下寻找。
  2. 下面roll_pointer指向的上一条记录trx_id为100,小于min_trx_id,所以返回了A事务修改的'Sun'。
可重复读举例

有数据1-Wang, 2-Yan, 3-Li,插入事务id均为80。

A事务id为100更新1 = Sun,不提交。

B事务id为200更新其他表,不提交。

C事务读id为1的数据,生成ReadView,m_ids=100, 200,min_trx_id=100, max_trx_id=201,creator_trx_id=0。

这时就该从版本链中开始寻找数据:

  1. id为1的数据目前的trx_id为100,100在m_ids中,说明那个事务在当前事务开始的时候还没执行完,不应该读出来,继续向下寻找。
  2. 此时roll_pointer指向的上次undo_log记录trx_id为80,80小于min_trx_id,说明这个事务在生成ReadView之前已经提交了,那就会读到'Wang'。

A事务提交。

B事务更新1 = Liu,不提交。

C事务读id为1的数据,由于是可重复读隔离级别,所以不再生成新的ReadView,还是用之前的,m_ids=100, 200,min_trx_id=100, max_trx_id=201,creator_trx_id=0。

从版本链中开始寻找数据:

  1. id为1的数据目前的trx_id为200,200在m_ids中,继续向下寻找。
  2. 下面roll_pointer指向的上一条记录trx_id为100,还是在m_ids中,继续向下寻找。
  3. 再一条记录trx_id为80,小于min_trx_id,所以返回了最初的'Wang'。
相关推荐
ClouGence4 分钟前
Oracle BLOB 实时同步为什么这么难?一次看懂背后的 5 个技术挑战
数据库·oracle
jnrjian5 分钟前
index skip scan 和oracle partition index 未加分区键类似
数据库
不剪发的Tony老师13 分钟前
SQLQueryStress:一款SQL Server查询压力测试工具
数据库·sqlserver·压力测试
minji...20 分钟前
MySQL数据库 (六) MySQL表的约束(下),自增长约束,唯一键约束,外键约束,索引
数据库·mysql·索引·外键·唯一键·外键约束·自增长约束
赵渝强老师27 分钟前
【赵渝强老师】崖山数据库的逻辑存储结构
数据库·oracle
沐籽李41 分钟前
从问答到执行:Biomni 如何重构生物医学研究工作流
数据库·agent·aidd·抗体设计·biomni
wangbing112541 分钟前
MySQL的另类语法
数据库·mysql
超哥--41 分钟前
B站视频内容智能分析系统(六):Text-to-SQL 结构化查询
数据库·sql·音视频
暗暗别做白日梦1 小时前
Redisson 和redis 实现延迟消息
数据库·redis·缓存
西凉的悲伤1 小时前
redis和数据库实现分布式锁
java·数据库·redis·分布式