mysql MVCC产生幻读原因及解决方式

事务隔离级别有四种,mysql默认使用的是可重复读,mysql是怎么实现可重复读的?为什么会出现幻读?是否解决了幻读的问题?

一、事务的隔离级别

Read Uncommitted(未提交读)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。读取未提交的数据,也被称之为脏读(Dirty Read)。该级别用的很少。

Read Committed(提交读)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变,换句话说就是事务提交之前对其余事务不可见。这种隔离级别也支持不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select查询可能返回不同结果。

Repeatable Read(可重复读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题(mysql彻底解决了幻读问题?请往下看)。

Serializable(可串行化)
这是最高的隔离级别,它强制事务都是串行执行的,使之不可能相互冲突,从而解决幻读问题。换言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

在MySQL的众多存储引擎中,只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。

什么是 MVCC ?

MVCC,全称 Multi-Version Concurrency Control ,即多版本并发控制。MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读.

当前读,快照读和MVCC的关系

MVCC 多版本并发控制是 「维持一个数据的多个版本,使得读写操作没有冲突」 的概念,只是一个抽象概念,并非实现 因为 MVCC
只是一个抽象概念,要实现这么一个概念,MySQL 就需要提供具体的功能去实现它,「快照读就是 MySQL 实现 MVCC
理想模型的其中一个非阻塞读功能」。而相对而言,当前读就是悲观锁的具体功能实现
要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC 模型在 MySQL 中的具体实现则是由 3 个隐式字段,undo 日志,Read View 等去完成的,具体可以看下面的 MVCC 实现原理

InnoDB存储引擎在数据库每行数据的后面添加了三个字段

在这里插入图片描述
6字节的事务ID(DB_TRX_ID)字段:
用来标识最近一次对本行记录做修改(insert|update)的事务的标识符, 即最后一次修改(insert|update)本行记录的事务id。至于delete操作,在innodb看来也不过是一次update操作,更新行中的一个特殊位将行表示为deleted, 并非真正删除。
7字节的回滚指针(DB_ROLL_PTR)字段:
指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。
如果一行记录被更新, 则 undo log record 包含 ‘重建该行记录被更新之前内容’ 所必须的信息。
6字节的DB_ROW_ID字段:
包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。
结合聚簇索引的相关知识点, 我的理解是, 如果我们的表中没有主键或合适的唯一索引, 也就是无法生成聚簇索引的时候, InnoDB会帮我们自动生成聚集索引, 但聚簇索引会使用DB_ROW_ID的值来作为主键; 如果我们有自己的主键或者合适的唯一索引, 那么聚簇索引中也就不会包含 DB_ROW_ID 了 。

MVCC逻辑流程-插入(insert)
同一事务中(假设事务id=1)插入两条记录,记录的数据版本号为事务id=1,删除版本号为null

idDB_TRX_ID(版本号)DB_ROLL_PT(删除版本号)
11null
11null

MVCC逻辑流程-查询(select)
  1、查找数据版本号,早于(小于等于)当前事务id的数据行。 这样可以确保事务读取的数据是事务之前已经存在的。或者是当前事务插入或修改的。
  2、查找删除版本号为null 或者大于当前事务版本号的记录。 这样确保取出来的数据在当前事务开启之前没有被删除。

MVCC逻辑流程-删除(delete)
  有一个事务中执行两次(1,2)查询(假设事务id=1)
  假设事务 id=1 刚执行到(1),此时有另外一个事务 id=2 执行了删除语句,会更新数据的删除版本号为当前事务id = 2 
  接着执行事务 id=1的事务(2),根据SELECT 检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行,表中id=1的行由于删除时间(删除事务的ID)大于当前事务的ID,所以事务 id=2 的(2)在执行的时候也会把表中 id=1 的数据检索出来.

idDB_TRX_ID(版本号)DB_ROLL_PT(删除版本号)
11null
112

MVCC逻辑流程-修改(update)
  可以理解为,当一个事务中 修改一条记录时, 是先复制该数据,新数据数据版本号为当前事务id,删除版本号为 null 。然后更新 原来数据的删除版本号为 当前事务id。

idDB_TRX_ID(版本号)DB_ROLL_PT(删除版本号)
113
11null

什么情况MVCC也会出现幻读?

时间事务1事务2
t1begin
t2select * from test
t3insert into test value(3,33)
t4select * from test
t5update test set name=‘333’ where id = 3
t6select * from test
t7commit

事务1开启事务,select只有两行记录,在T3时刻,事务2插入一条数据并且马上提交,在T4时刻再次select读到与T2时刻一样的数据,到这是没有问题的,满足可重复读。
在T5时刻update一条没有的数据,却执行成功了,在T6时刻select却发现多了一个记录,这就出现幻读的现象。

Mysql 行锁、间隙锁和next-key锁

InnoDB支持几种不同的行级锁,MyISAM只支持表级锁
行锁(Record Lock): 对索引记录加锁。
间隙锁(Gap Lock): 锁住整个区间,包括:区间里具体的索引记录,不存在的空闲空间(可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引记录之后的空间)。

next-key锁: 行锁和间隙锁组合起来。

注意:如果检索条件不是索引的话会全表扫描,则是表级锁,不是行级锁

行锁(Record Lock)

对主键或者唯一索引进行增删改或显示的加锁,InnoDB会加行锁,如:

-- 显示的加锁
select * from  people where id =3 for update;
update people set name='James' where id=3

正常的查询语句使用的是共享锁。
对于显示的加锁或增删改操作,条件判断必须是精确匹配(也就是=) ,不能用>,<,between或like等范围查询方式,因为这样会使行锁变成next-key Lock。

间隙锁(Gap Lock)

官方文档描述:Gap Lock的唯一目的就是阻止其他事务插入到间隙中。Gap Lock可以同时存在,不同的事务可以同时获取相同的Gap Lock,并不会互相冲突。Gap Lock也是可以显示的被禁止的,只要将事务的隔离级别降低到 READ COMMITTED。

对于间隙锁,什么叫锁住不存在的空闲空间,举个例子:
一个表有id为1,2,3,5,6,9行数据,执行如下sql语句

select * from  people where id > 3 AND id <7 for update;

这是一个范围检索,InnoDB不仅会锁住id为5和6两行的数据,也会锁住id为4(虽然该行并不存在)的纪录。

next-key Lock

官方文档描述:Record Lock+Gap Lock,如果一个事务在记录R上的某个索引有共享/互斥锁,也会对其前面一个范围加锁

锁定的区域
根据索引会形成一个个左开右闭的一个区间,根据查询的条件其所在的区间,并且包括其后的区间。

这里给出一个people表

idnameage
1james37
2tony28
3li34

如果age是索引的话,相关的区域有
(-无穷,28]
(28,34]
(34,37]
(37,+无穷)

如果执行如下语句:

select * from  people where age =34 for update;

那么会锁住(28,37]这么范围

select * from  people where age =33 for update;

那么会锁住(28,34)这么范围

间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
(1)防止间隙内有新数据被插入
(2)防止已存在的数据,更新成间隙内的数据

innodb自动使用间隙锁的条件:
(1)必须在Repeatable Read级别下
(2)检索条件必须有普通索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)

注意:这里的普通索引不包括主键索引和唯一索引,如果在这两个索引下因为能精确检索出结果,所以会使用Record Lock直接锁定具体的行(范围查询除外)。

如何解决幻读

在快照读情况下,mysql通过mvcc来避免幻读。
在当前读情况下,mysql通过X锁或next-key来避免其他事务修改:

1.使用串行化读的隔离级别
2.(update、delete)当where条件为主键时,通过对主键索引加record locks(索引加锁/行锁)处理幻读。
3.(update、delete)当where条件为非主键索引时,通过next-key锁处理。next-key是record locks(索引加锁/行锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)的结合。
在这里插入图片描述

  • 6
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值