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'。
相关推荐
乌鸦乌鸦你的小虎牙1 小时前
qt 5.12.8 配置报错(交叉编译环境)
开发语言·数据库·qt
一只大袋鼠2 小时前
Redis 安装+基于短信验证码登录功能的完整实现
java·开发语言·数据库·redis·缓存·学习笔记
Anastasiozzzz2 小时前
深入研究Redis的ZSet底层数据结构:从 Ziplist 的级联更新到 Listpack 的完美救场
数据结构·数据库·redis
菠萝蚊鸭2 小时前
x86 平台使用 buildx 基于源码构建 MySQL Wsrep 5.7.44 镜像
数据库·mysql·galera·wsrep
沙漏无语4 小时前
(二)TIDB搭建正式集群
linux·数据库·tidb
姚不倒4 小时前
三节点 TiDB 集群部署与负载均衡搭建实战
运维·数据库·分布式·负载均衡·tidb
隔壁小邓4 小时前
批量更新方式与对比
数据库
数据知道4 小时前
MongoDB复制集架构原理:Primary、Secondary 与 Arbiter 的角色分工
数据库·mongodb·架构
人道领域4 小时前
苍穹外卖:菜品新增功能全流程解析
数据库·后端·状态模式
修行者Java4 小时前
(七)从 “非结构化数据难存储” 到 “MongoDB 灵活赋能”——MongoDB 实战进阶指南
数据库·mongodb