彻底了解InnoDB的各种读这篇文章就够了

文章目录

      • 1. InnoDB的事务
        • 事务的四大特性
        • InnoDB的四种隔离级别
        • 隔离级别产生的读问题
      • 2. undo log
        • 更新数据
      • 3. readView
        • 概念
        • 可见性规则
        • 基础数据
      • 4. 脏读产生原因
      • 5. 不可重复读产生原因
      • 6. 不可重复读解决
      • 7. 幻读产生原因
      • 8. 如何解决幻读问题

1. InnoDB的事务

InnoDB引擎与其他mysql引擎最大的区别是其引入了事务的概念,使用事务可以保证

事务的四大特性

  1. 原子性: 要么全部成功,要么全部失败。
  2. **隔离性:**各个事务的执行不能被其他事务干扰。
  3. 持久性: 操作执行结果会被持久化到磁盘。
  4. 一致性: 事务执行结果必须从一个一致状态到另一个一致状态,中间不能有其他状态。

原子性,持久性,隔离性的目的也是为了保障数据的一致性!

InnoDB的四种隔离级别

  1. **READ UNCOMMITTED 读未提交 (RU) **会产生脏读
  2. READ COMMITTED 读已提交 (RC) 会产生不可重复读
  3. REPEATABLE READ 可重复读 (RR) 会产生幻读
  4. **SERIALIZABLE 串形化 ** 不会有读问题,但是性能较差

隔离级别产生的读问题

**脏读:**读到其他事务未提交的数据。

不可重复读: 读到其他事务已经提交的事务,导致一次事务中,两次查询同一条记录不一致。

幻读: 幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。

2. undo log

​ 如果看过笔者的《mysql专栏》undo log页 会知道InnoDB使用undo log来实现事务,当每一个事务操作数据的时候 会在undo log的链表中生成修改之前的数据记录,除了记录数据,其会记录两个隐藏列

  • DB_TRX_ID:对该条记录进行修改的事务id
  • DB_ROLL_PT:回滚指针,指向该事务之前的undo log。
  • DB_ROW_ID:包含一个行ID,该ID在插入新行时会单调增。

在这里插入图片描述

更新数据

下面以多个事务更新同一条数据为例,看看undo log是如何工作的

//插入并提交事务
INSERT INTO USER VALUES (1, 'tony',20);

在这里插入图片描述

//事务A更新数据
UPDATE user SET name='jack' WHERE id=1;

//事务B更新数据
UPDATE user SET name='tom' WHERE id=1;
//事务C更新数据
UPDATE user SET name='blob' WHERE id=1;

上述三个事务执行后 undo log的链表如下:

彻底了解InnoDB的各种读这篇文章就够了_第1张图片

3. readView

概念

​ InnoDB支持MVCC多版本,其中RC(Read Committed)和RR(Repeatable Read)隔离级别是利用consistent read view(一致读视图)方式支持的。 所谓consistent read view就是在某一时刻给事务系统trx_sys打snapshot(快照)。ReadView 其实就是一个保存事务ID的list列表,其中包含如下主要部分:

  • **m_ids:**当前有正在执行的事务列表;
  • min_trx_id: 是指 m_ids 里最小的值;
  • **max_trx_id: **表示生成ReadView时系统中将要分配给下一个事务的id值。
  • creator_trx_id: 表示生成该ReadView的事务的事务id。

前三个属性将事务列表分成如下三个区间

彻底了解InnoDB的各种读这篇文章就够了_第2张图片

通过比较当前每次readView时候创建的事务creator_trx_id 在当前区间的位置,则可以判断其可见性。

可见性规则

1、trx_id < m_ids列表中最小的事务id
表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
2、trx_id > m_ids列表中最大的事务id
表明生成该版本的事务在生成ReadView 后才生成,所以该版本不可以被当前事务访问。
3、m_ids列表中最小的事务id < trx_id < m_ids列表中最大的事务id
此处比如m_ids为[5,6,7,9,10]
①、若trx_id不在m_ids中,比如是8:说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
②、若trx_id在m_ids中,比如是6,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问。不可访问后需要获取undo log链的上一个节点中的trx_id继续上述可见性判断。

事务可见性分析

其实了解了InnoDB的undo log链和readVieew快照后,innoDB的事务隔离级别产生的问题也就清楚了,主要体现不同隔离级别下产生的readView 快照时机不同,导致其获取到的undo log 节点不同。

基础数据

-- 创建表
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(50) DEFAULT NULL COMMENT '姓名',
  `age` int(3) DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;

-- 插入数据
insert  into `user`(`id`,`name`,`age`) values (1,'tony',20);

-- 查看innoDB的自动提交
SHOW VARIABLES LIKE 'autocommit';
-- 关闭innoDB的自动提交
SET autocommit = 0

-- 查看全局隔离级别:
SELECT @@global.tx_isolation;

4. 脏读产生原因

-- 设置事务隔离级别为 读未提交
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; 
事务A 事务B
BEGIN; BEGIN;
INSERT INTO user (id, name, age) VALUES(‘2’,‘blob’,‘25’);
SELECT * FROM USER; //脏读产生,查询到事务A未提交的数据
COMMIT; COMMIT;

如上所示,脏读现象发生了,是因为 InnoDB在 READ UNCOMMITTED(读未提交)是不会产生readView快照的,所以会获取到undo log链的最新数据,所以无论是新增、修改等等都会被立马获取到,没有可见性的判断。

彻底了解InnoDB的各种读这篇文章就够了_第3张图片

5. 不可重复读产生原因

-- 设置全局事务隔离级别为 读已提交
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; 
事务A 事务B 节 点
START TRANSACTION; START TRANSACTION;
UPDATE user SET age =30 WHERE id =1;
SELECT * FROM USER WHERE id = 1; //此时查询不到事务A未提交的数脏读避免了。
COMMIT; **
SELECT * FROM USER WHERE id = 1; //不可重复读产生,事务A提交后,再次查询到了修改的数据,同一个事务里两次查询数据不一致
COMMIT;

下面我们来分析一下事务B两次读获取id=1的数据不同原因(主要的分析依据上述章节的2.undo log和3.readView),

不同于read uncommited(读未提交),read commited隔离级别下,每一次读取数据时候都会生成一个readView快照

① 事务A和事务B开启,假定当前活跃事务从5开始,则活跃的事务列表中m_ids[5(事务A), 7(事务B), 10(其他事务) ]

② undo log链条添加一条id=1的元素修改之前的数据

③ 针对此次查询事务B生成一次快照readView 包含数据如下

  • m_ids:[5(事务A), 7(事务B), 10(其他事务)]。
  • min_trx_id: 5(事务A)。
  • **max_trx_id: **最大是10 则下一个事务id 为11。
  • creator_trx_id: 7(事务B)。

此时通过可见性判断,当前事务id=7 属于 m_ids列表中,最新的undo log链查找属于本事务id的undo log数据如果没有,则此事务在活跃中不可访问,需要从undo log连中查找上一个4(此时undo log只有一条事务Aid=5的链条)。4是已经提交的事务则可以直接访问初始数据。则不会查找事务A未提交的数据,避免了脏读

④ 事务A 提交 则活跃事务列表变成了 m_ids[7(事务B), 10(其他事务) ]

⑤ 同3 事务B再次生成快照readView,m_ids:[7(事务B), 10(其他事务)],min_trx_id: 7(事务B),max_trx_id: 11,可见性获取事务id=5已提交,则事务B再次获取到了id=1的新数据,两次相同查找条件获取到了不同的结果,出现了不可重复读
彻底了解InnoDB的各种读这篇文章就够了_第4张图片
彻底了解InnoDB的各种读这篇文章就够了_第5张图片

6. 不可重复读解决

-- 设置全局事务隔离级别为 读已提交
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; 

还是5中的例子,则可以看到事务B多次查询(事务A 提交前后)查询结果一致,这是因为在REPEATABLE READ(可重复读)隔离级别下实在每次事务开始的时候生成一次readView快照。

彻底了解InnoDB的各种读这篇文章就够了_第6张图片

7. 幻读产生原因

​ 幻读并不是说事务中多次读取获取的结果集不同,而是一次查询后数据不存在,在插入该数据,出现数据重复的错误,

幻读: 某次的 select 操作得到的结果集所表征的数据状态无法支撑后续的业务操作。幻读也好理解其实就是不可重复读只看事务开始的时候生成readView,不管其他事务是否提交了数据,导致自己插入重复数据。

事务A 事务B
START TRANSACTION; START TRANSACTION;
SELECT * FROM user where id =6;// 此时查询不到id=6的记录
INSERT INTO user (id, name, age) VALUES(‘2’,‘blob’,‘25’)
COMMIT;
SELECT * FROM user where id =6;// 此时查询不到id=6的记录
INSERT INTO user (id, name, age) VALUES(‘2’,‘blob’,‘25’) //幻读产生出现数据重复key错误
COMMIT;

8. 如何解决幻读问题

​ 解决幻读问题就是通过加锁来解决,但是innoDB只有

  • 行锁(锁住一行),行锁无法处理幻读的间隙插入问题,无法适用。
  • 表锁(锁住全表),锁表可以避免幻读,但是开销过高,性能差。

对此InnoDB提供了一个间隙锁:主键/唯一索引查询只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁。

查询表现形式为

select * from 【table】 where 【condition】 for update;
~~假设user表存在 id 为[0,5,15,20]

查询id=3或者查询唯一索引u=3,会产生间隙锁(0,5)

查询id>3 and id <8,会产生间隙锁(0,5],(5,10]

上述幻读例子中 查询修改为: 此时事务

-- 此时6不存在 此时会产间隙锁5~6的间隙锁 
SELECT * FROM `user`  where id =6 for update;
事务A 事务B
START TRANSACTION; START TRANSACTION;
SELECT * FROM user where id =6 FOR UPDATE;// 产生间隙锁 锁住了id 【5,6】的间隙锁
INSERT INTO user (id, name, age) VALUES(‘2’,‘blob’,‘25’) //此时会阻塞直到事务B提交
COMMIT;
SELECT * FROM user where id =6;// 此时查询不到id=6的记录
INSERT INTO user (id, name, age) VALUES(‘2’,‘blob’,‘25’) //正常插入避免幻读
COMMIT;

你可能感兴趣的:(mysql,脏读,不可重复读,幻读,readView,undo,log)