Skip to main content
  1. posts/

彻底搞懂幻读

·1288 words·3 mins

前置内容

事务隔离级别

未提交读 读已提交【RC】 可重复读(默认隔离级别)【RR】 序列化

MVCC

多版本并发控制(在没行记录后面增加插入版本号和删除版本号)

Mysql读取行的方式

快照读

如果事务 隔离级别为 REPEATABLE READ(默认级别),则同一事务中的所有一致读取都会读取该事务中第一个此类读取建立的快照。您可以通过提交当前事务并在之后发出新查询来获取更新的查询快照。 通过READ COMMITTED隔离级别,事务中的每个一致读取都会设置并读取其自己的最新快照。

来自mysql官方文档 17.7.2.3 一致的非锁定读取https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html

当前读

读取最新的行(select for update. 会读取到最新的行。)

幻读是什么

在同一个事务中,同一个查询执行多次所看到的数据不一致。

幻读怎么产生的

快照读场景

每次查询都是基于快照查询,后面的修改是无法查询到的,所以不存在幻读问题。

当前读场景

当前读只会锁住被查询的行。 但是如果这时候更新了其他行,导致满足查询条件,就也会同样出现在查询结果中。 这样的话,查询结果就不一致了。但是在当前读下,读到最新的数据看起来是合理的。 如果我改一下数据,就会把后面事务更新的数据给更新掉,导致前一个小的事务id更新了后一个大的事务id的数据。这就不合理了。

image.png

幻读会导致什么问题

  1. 当前读数据不一致(范围查询)
  2. 更新序列错乱

怎么解决幻读

了解了幻读的产生,就知道要怎么解决了。 幻读产生的两个根本原因:

  1. 更新其他行数据满足查询条件
  2. 增加新行

mysql引入了间隙锁来解决幻读问题 下一键锁 = 行锁 + 间隙锁

问题:

  1. mysql是按照主键进行数据追加的。如果查询不按主键查询,那要怎么防止其他事务更新的数据满足查询条件呢?(间隙是指两行数据直接的间隙吗?或者对索引也加锁。)
  2. 如果间隙是指两行数据直接的间隙,那不就是类似于锁全表吗?

eg:

create table t(
a int primary key,
b int ,
c int
);
insert into t values(1,1,1);
insert into t values(2,2,5);
insert into t values(3,3,10);
insert into t values(4,4,16);
ABC
begin
select * from t where c>=10 for update;
insert into t values(5,5,28);
[blocked]
update t set c = 10 where a = 1;
[blocked]
select * from t where c>=10 for update;
commit

实测结果: 事务B和C都会被锁住。【所以这个间隙大概率是两行记录直接的间隙】

会引入什么新的问题

并发插入问题[间隙锁可以在两个事务中存在]

SessionASessionB
select * from t where id = 9 for update
select * from t where id = 9 for update
insert into t values(8,8,8) [blocked]
insert into t values(10,10,10) [blocked]
dead lock

究竟应该如何设置隔离级别

既然可重复读处理这么麻烦也存在问题,那么究竟应该怎么设置隔离级别呢? 目前也有这样的玩法

  1. 读已提交 + binlog=row(用于处理数据不一致的情况)
  2. 可重复读

业务思考

读已提交隔离级别会有什么问题?

【数据不一致】 但其实我们大部分情况下,都是一次性事务。而且基本查询都是快照读。 快照读在查询会不会有问题,举个例子,

事务1:事务2
创建订单
查询订单
向三方查询
查询失败
查询订单状态为新建
订单成功了-回调
更新订单状态为成功
更新订单状态为失败

怎么解决呢? 查询和更新放在同一个事务中,查询改为当前读。 所以还是需要可重复读级别啊

同一个事务,先查询,然后根据数据状态更新数据会不会有问题?

如上,还是会有问题的。

什么业务应该设置为可重复读?

对数据一致性要求没那么高的业务。