InnoDB解决幻读了吗?

目录

前言

验证

环境信息

数据准备

当前读

场景

验证结果

​编辑

结论

快照读 

场景1

 验证结果​编辑

场景2

 验证结果​编辑

场景3(产生幻读)

 验证结果​编辑

结论

幻读分析

步骤1(初始化数据)

步骤2(开启事务A,进行第一次快照读)

 步骤3(开始事务B,插入一条新数据,提交事务)

步骤4(事务A执行更新语句) 

步骤5(事务A进行第二次快照读,提交事务A)

Q&A


前言

本文在探讨InnoDB是否真正意义上解决幻读需要相关的前置知识

InnoDB中的事务隔离级别和锁的关系

搞懂MySQL的MVCC

验证

环境信息

MySQL版本:8.0.36

存储引擎:InnoDB

隔离级别:REPEATABLE-READ

数据准备

CREATE TABLE `user_info` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `age` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `user_info` VALUES (1, '测试1', 10);
INSERT INTO `user_info` VALUES (2, '测试2', 15);
INSERT INTO `user_info` VALUES (3, '测试3', 18);
INSERT INTO `user_info` VALUES (4, '测试4', 24);

数据如下:

当前读

场景

  • 开启事务A
  • 在事务A中通过select for update进行当前读查询(age > 15)
  • 开启事务B
  • 在事务B中插入一条新数据
  • 提交事务B
  • 在事务A中再次通过select for update进行当前读查询(age > 15)
  • 提交事务A
验证结果

SELECT OBJECT_NAME,INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_DATA FROM `performance_schema`.data_locks;

结论

由场景验证结果可以看到,由于Next-Key Lock的存在,会阻塞其他事务对加锁间隙的数据插入,所以当前读场景下通过LBCC是可以完全解决幻读的问题。

快照读 

场景1

  • 开启事务A
  • 在事务A中通过select进行快照读查询(age > 15)
  • 开启事务B
  • 在事务B中插入一条新数据
  • 提交事务B
  • 在事务A中再次通过select进行快照读查询(age > 15)
  • 提交事务A
 验证结果

由场景1验证结果来看,RR隔离级别是解决了幻读问题,但是并没有完全解决,接着看场景2。

场景2

  • 开启事务A
  • 在事务A中通过select进行快照读查询(age > 15)
  • 开启事务B
  • 在事务B中插入一条新数据
  • 提交事务B
  • 在事务A中将age=18的所有数据的name改成测试99
  • 在事务A中再次通过select进行快照读查询(age > 15)
  • 提交事务A
 验证结果

由场景2来看,在第二次快照读查询的时候只是age=18的数据产生了变化,并没有查询到事务B中插入的数据,由此可见该场景依然解决了幻读问题(事务A中的UPDATE没修改到事务B中新插入的数据)

场景3(产生幻读)

  • 开启事务A
  • 在事务A中通过select进行快照读查询(age > 15)
  • 开启事务B
  • 在事务B中插入一条新数据
  • 提交事务B
  • 在事务A中将age=16的所有数据的name改成测试99
  • 在事务A中再次通过select进行快照读查询(age > 15)
  • 提交事务A
 验证结果

由场景3中可以看到,在第二次快照读查询中,查询到了事务B所新插入的数据,产生幻读(事务A中的UPDATE修改到事务B中新插入的数据)

结论

从以上3个场景可以看出,在某些特殊场景下会产生幻读,因此MVCC并没有完全解决幻读的问题。

下面我们就来一探究竟产生幻读的原因。

幻读分析

从验证结果来看,当前读是可以完全避免幻读问题,而对于快照读如果在两次读取之间进行了当前读,在某些情况是会触发幻读的问题(场景3),那么我们基于场景3结合MVCC一步步进行分析幻读产生的原因。

步骤1(初始化数据)

将测试数据的插入语句放在同一个事务中,让数据的DB_TRX_ID保持一致(方便后续分析)。

BEGIN;
INSERT INTO `user_info` VALUES (1, '测试1', 10);
INSERT INTO `user_info` VALUES (2, '测试2', 15);
INSERT INTO `user_info` VALUES (3, '测试3', 18);
INSERT INTO `user_info` VALUES (4, '测试4', 24);
COMMIT;

在事务COMMIT之前查询下当前事务id

测试数据DB_TRX_ID都为802963,数据结构如下:

步骤2(开启事务A,进行第一次快照读)

开启事务A

BEGIN;

进行第一次快照读

#不会分配事务id,文章末尾进行讲解
select * from user_info where age > 15;

此时会生成一张ReadView读视图,m_ids,m_up_limit_id,m_low_limit_id,m_creator_trx_id的值分别为:

m_ids:无

m_up_limit_id:无

m_low_limit_id:生成 ReadView 时系统将要分配给下一个事务的 id 值,这里具体也不知道分配的id值是多少,但是值肯定是大于802963的(这里假设值为802964)。

m_creator_trx_id:无

根据ReadView可见性判断规则可以得出此时查询出的数据为id=3和id=4的数据。

注:可见性判断规则

  1. 被访问版本的 trx_id == m_creator_trx_id,说明当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

  2. 被访问版本的 trx_id < m_up_limit_id,说明被访问版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。

  3. 被访问版本的 trx_id ≥ m_low_limit_id,说明被访问版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。

  4. 访问版本的 trx_id是否在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

 步骤3(开始事务B,插入一条新数据,提交事务)

BEGIN;
INSERT INTO user_info(name, age) VALUES('测试5', 16);
COMMIT;

在事务COMMIT之前,我们先查询下当前的事务id

 

那么在事务B COMMIT之后,当前表里的数据应当如下:

步骤4(事务A执行更新语句) 

#这里才真正分配了事务id
UPDATE user_info SET name = '测试99' WHERE age = 16;

更新时,我们查询下当前的事务id(这里事务A的事务id在执行此句更新语句后才真正分配,并且由于事务B先分配了事务id,因此这里trx_id比事务B的trx_id大是正确的)。

执行完更新后,此时表里的数据发生了变化,具体数据如下图:

步骤5(事务A进行第二次快照读,提交事务A)

SELECT * FROM user_info WHERE age > 15;
COMMIT;

 此时,在进行第二次快照读的时候发生了幻读。由于我设置的事务隔离级别为RR,那么在进行第二次快照读的时候会复用第一次的ReadView,那么id=5的数据undolog日志如下

根据ReadView可见性规则判断

被访问版本的 trx_id(804618) == m_creator_trx_id(804618),说明当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

因此该条数据会展现出来,产生幻读。

Q&A

Q:快照读的场景2和场景3都执行了当前读的更新操作,那么为什么场景2没有产生幻读?

A:因为场景2的当前读更新操作更新的是age=18的这条数据,age=16的这条数据DB_TRX_ID还是为803552,那么根据ReadView可见性规则判断

被访问版本的 trx_id(803552) ≥ m_low_limit_id(802964),说明被访问版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问

Q:为什么事务A在进行第一次快照读的时候没有生成事务id?

A:在执行第一次快照读的时候,会以读事务的身份启动新事务(读事务不会分配事务id)

可以看出,是个非常大的数字(大于等于 2*48,说明该事务没有分配 ID)。

读写事务在创建时并不会立马分配 事务id,而是等到事务执行第一条更改语句(INSERT、UPDATE、DELETE)时才分配 事务id。

Q:假设当前表的测试数据如下,如果在事务A执行了select * from user_info where age = 16 FOR UPDATE,那么如果我在事务B插入age为15和age为18的数据能插入吗?

A:事务A执行当前读后,首先看下当前的Next-Key Lock锁住的数据范围

由上图可以看出Next-Key Lock退化成了间隙锁,锁住的范围为(15,18),那么理论上插入age=15和age=18是能插入的,接下来我们做下测试。

age=15

 

age=18

可以看到age=15的数据插入失败了,而age=18却插入成功了,这是为什么呢?

这个涉及到了B+树的原理。由于age为非聚簇索引,如果出现索引值相同的情况下,会按聚簇索引值进行有序排列。由于我们主键设置为自增,因此插入的age=15在锁住的范围内,而age=18不在范围内。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值