摘要
通过创建版本号、删除版本号,让每一次增删改操作都可以复制一份快照,而查询操作通过条件过滤,再加上版本过滤,得到对应事务隔离级别的最终数据。
基础概念
Mysql默认的隔离级别是 RR,可重复读。实现原理就是MVCC。下面看看MVCC的原理。
操作示例
建表语句
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for test
-- ----------------------------
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`value` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
Mysql在创建表的时候,会多创建三列,其中两列是跟 MVCC相关的。
数据行的版本号 (DB_TRX_ID) (为了方便看,就叫create_version)
删除版本号 (DB_ROLL_PT) (为了方便看,就叫 delete_version)
所以,上面的表结构实际上是这样子的。后面的示例中 忽略 未知列
id
value
create_version
delete_version
未知列
查看MYSQL的默认隔离级别
SELECT @@global.tx_isolation, @@tx_isolation;
@@global.tx_isolation : 全局事务隔离级别 @@tx_isolation : 当前回话的隔离级别
更改MYSQL隔离级别
# 设全局事务为 RU 读未提交的隔离级别
SET @@global.tx_isolation = 0;
SET @@global.tx_isolation = 'READ-UNCOMMITTED';
# 设置全局事务为 RC 读已提交的事务隔离级别
SET @@global.tx_isolation = 1;
SET @@global.tx_isolation = 'READ-COMMITTED';
# 设置全局事务为 RR 可重复读的隔离级别
SET @@global.tx_isolation = 2;
SET @@global.tx_isolation = 'REPEATABLE-READ';
# 设置全局事务为 串行化 隔离级别
SET @@global.tx_isolation = 3;
SET @@global.tx_isolation = 'SERIALIZABLE';
MVCC 插入原理
假设当前全局事务号是 10,插入一条数据。
begin; -- 获取事务id 为10
insert test (id ,value) value (1,1);
commit;
物理存储如下:
id
value
create_version
delete_version
1
1
10
NULL
再插入一条数据,这时候全局事务id是11,
begin; -- 获取事务id 为 11
insert test (id ,value) value (5,5);
commit;
物理存储如下:
id
value
create_version
delete_version
1
1
10
NULL
5
5
11
NULL
上面是插入的两条数据在mysql物理文件中存储的物理数据。这时候还只有create_version,还没有delete_version
MVCC 删除原理
删除数据,并没有真正的删除数据,只是在原数据行的delete_version上插入当前的事务版本号。
begin; -- 假设这次全局事务id 是 12
delete from test where id = 5;
commit;
这时候,数据的物理文件如下:
id
value
create_version
delete_version
1
1
10
NULL
5
5
11
12
MVCC 修改原理
修改语句
begin; -- 开启事务,假设全局事务id号 是 13
update test set value = 2 where id = 1;
commit;
数据的物理存储如下:
id
value
create_version
delete_version
1
1
10
13
5
5
11
12
5
5
13
NULL
MVCC 查询原理
这个时候查询
select * from test where id >= 1;
这时候查到的结果是:
查询是怎么做的呢?
此时,数据查询规则如下:
被查找的数据行的创建事务号不超过当前事务的创建事务号
上面这句话,说明查询出来的数据行,事务版本号要小于或者等于当前事务的版本,确保数据是在本事务之前或者在本次事务中创建的。不能是在本次事务之后的事务中创建的数据。
被查找的数据行删除事务号要么为NULL,要么大于当前事务号。
只有这样,才能保证查询的数据是没有被删除的(删除事务号为NULL) 或者 在本次事务之后的事务中被删除的(大于当前事务号)
脏读场景
这种场景只会在 RU(Read uncommitted 未提交读) 隔离级别中出现。
这种场景需要先将 事务的隔离级别设置为 RU(Read uncommitted 未提交读)。
这个也用 不可重复读的场景例子,跟不可重复读的区别在,B事务执行插入之后,不需要提交事务,A事务就能查询到数据了。
不可重复读场景
这种场景会在 RC(Read committed 已提交读) 隔离级别中出现。
先将全局事务隔离级别设置为 RC级别。
场景:先开启事务A,然后开启事务B,事务B插入数据,之后事务A去查询,这时候RR隔离级别应该查询不到这种条数据,RC隔离级别能查询到这条数据。不可重复读场景能查询到这条数据。
事务A:
begin ; -- 第1步
select * from test ; 第 2步
select * from test ; 第3步
commit; 第4步
先开启A事务,执行第1步,第2步,查询到的结果如下:
接着开启事务B,执行 第1步、第2步
begin; -- 第1步
insert into test (id ,value) value(7,7); -- 第2步
commit; -- 第3步
这时候再执行事务A的第3步,还是看不到7这条数据。接着执行B的第3步,这时候再执行A的第4步,这时候就能看到7这条数据了。
幻读场景
这种场景会在 RR(REPEATABLE READ 可重复读) 隔离级别中出现。
MYSQL默认的就是这种隔离级别。我们先就这种场景举例如下(这里id建立了唯一索引):
业务需求:表中如果不存在id=6的数据,则插入
事务A执行步骤:
begin; -- 第1步
select * from test where id = 6 ; -- 第2步
-- 业务判断 发现 不存在
insert into test (id ,value) value (6,6); -- 第3步
commit ; -- 第4步
事务B执行步骤:
begin; -- 第1步
insert into test (id ,value) value (6,6); -- 第2步
commit ; -- 第3步
假设A事务执行到了第1步,B事务执行到了第2步,接着执行A事务,这时候A事务第2步发现没有id为6的数据,但是第3步却执行不了,一直挂在锁等待状态。
这种场景,A事务是看不到id=6的数据,但是A事务也不能执行插入操作,有魔幻的感觉,这就是幻读。
参考文档