InnoDB基于MVCC和next-key锁解决幻读问题

事务的ACID

  • 原子性:整个事务中的所有操作要么全部提交成功,要么全部失败回滚;
  • 一致性:总是从一个一致性的状态转换到另外一个一致性的状态,无中间状态;
  • 隔离性:一个事务所做的修改在最终提交以前,对其他事务是不可见的;
  • 持久性:一旦事务提交,其所做的修改就会永久保存到数据库中,即使系统崩溃,数据也不会丢失。

事务的隔离级别

  • 未提交读:事务中的修改,即是没有提交,对其他事务也都是可见的,会出现脏读;
  • 提交读:一个事务开始时,只能看见已经提交的事务所做的修改,也叫不可重复读;
  • 可重复读:保证了再同一个事务中多次读取同样记录的结果是一致的,会出现幻读;
  • 可串行化:会在读取的每一行数据上都加锁,可能导致大量的超时和锁竞争问题。
隔离级别脏读可能性不可重复读可能性幻读可能性加锁读
未提交读YesYesYesNo
提交读NoYesYesNo
可重复读NoNoYesNo
可串行化NoNoNoYes

写锁比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面。

共享锁(读锁):多个客户在同一时刻可以同时读取同一个资源,二互不干扰;
排他锁(写锁):一个写锁会阻塞其他的写锁和读锁。

锁粒度

尽量只锁定需要修改的部分数据,而不是所有资源,锁定的数据量越少,则系统的并发程度越高。

表锁:它会锁定整张表,一个用户对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之前是不相互阻塞的;
行级锁:最大程度支持并发处理,只在存储引擎层面实现,而MySQL服务器层没有实现。

多版本并发控制(MVCC)

不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现。这两个列,一个保存了行的创建时间,一个保存行的删除时间,并不是实际的世界,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号作比较,MVCC只在可重读读和提交读两种隔离级别下工作。

可重复读下的MVCC的操作方法

SELECT

InnoDB会根据以下两个条件检查每行记录:

  1. InnoDB只查询版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的);
  2. 行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务之前未被删除。

只有符合上述两个条件的记录,才能返回作为查询结果。

INSERT

InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

DELETE

InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE

InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。

可重复读下,MVCC的幻读问题

读操作不会出现幻读

关闭自动提交事务

SET AUTOCOMMIT = 0;

新增测试表测试数据
在这里插入图片描述
开启事务A,读取到两条测试数据
此时读取到原有的两条测试数据
开启事务B,插入数据并提交
在这里插入图片描述
事务A再次读取仍为两条数据,没有出现幻读,注意不要执行begin,单独执行查询,否则开启了新的事务
在这里插入图片描述

更新操作会出现幻读问题

在测试表上新增一个测试字段,测试表和测试数据在这里插入图片描述
开启事务A,更新数据,受影响行数3条
在这里插入图片描述
开启事务B,插入数据并提交
在这里插入图片描述
事务A再次更新数据,却仍然影响了事务B插入的数据,出现幻读,注意不要执行begin,单独执行更新,否则新开启了事务
在这里插入图片描述

这种现象的原因

快照读

当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。

当前读

对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的版本号记录,写操作后把版本号改为了当前事务的版本号,所以即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。也正是因为这样所以才导致幻读。

  • 在快照读情况下,MySQL通过mvcc来避免幻读。
  • 在当前读情况下,MySQL通过next-key来避免幻读

如何解决当前读导致的幻读问题

使用可串行化的隔离级别

SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁竞争的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别

使用next-key锁

  • 行锁:单个行记录的锁,主键和唯一索引都是行记录的锁模式,避免其它事务执行更新操作时,导致当前事务发生幻读;

  • 间隙锁:间隙锁是索引行上的一段开区间,间隙锁基于非唯一索引实现,由于InnoDB中索引是有序的,当前事务基于非唯一索引更新数据时InnoDB会在非唯一索引上加上间隙锁,阻塞其他事务需要插入的数据行,避免其它事务执行插入操作时,导致当前事务发生幻读;

  • next-key锁:next-key锁是索引行上的一段前开后闭的区间,是MySQL加锁的基本单位,next-key锁也是行锁和间隙锁的组合。

    新增非唯一索引

ALTER TABLE tb_user ADD INDEX idx_batch(batch);

开启事务A,更新数据,受影响行数3条
在这里插入图片描述
开始事务B插入数据,此时由于next-key锁存在,插入被阻塞
在这里插入图片描述
所以在InnoDB中,在可重复读隔离级别中,MVCC可防止快照读引起的幻读,next-key锁可防止当前读引起的幻读

使用next-key锁不能完全解决幻读问题

当快照读和当前读同时发生时,请看下面例子:
原始数据:
在这里插入图片描述
开始事务A,快照读查询数据,此时读取到3条数据
在这里插入图片描述
开启事务B,插入数据并提交事务
在这里插入图片描述
返回事务A,更新数据后,再次快照读取数据,居然读到了事务B提交的数据
在这里插入图片描述
这种情况下幻读发生的原因:更新操作是当前读,更新操作后使得MVCC版本控制失效了,快照读出现了幻读

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值