mysql 快照读
Published on: | Views: 99问题引入
有次小A问我,他有一段代码,明明加了分布式的锁,但有时候仍然插入了两条数据,是为什么? 已知数据库为mysql, 引擎为innodb, 隔离级别为Repeatable Read, 他的伪代码如下:
@Transcational
public void checkAndInsert(){
Object a = executeQuery("select * from table_a");
if(a==null){
distribute_lock();
a = executeQuery("select * from table_a");
if(a == null){
executeUpdate("insert into table_a ...");
}
distribute_unlock();
}
}
首先他在函数外面开启了事务,然后查询一次有没有数据,如果没有就加锁,再查询一次,还没有就插入数据,看起来没有什么问题,但为什么并发执行会插入两条数据呢? 这就不得不提到mysql数据库的一致性读问题了。
地球人都知道,mysql的innodb引擎使用了MVCC, 对于事务中的查询,在隔离级别Repeatable Read和Read Committed下面,它提供了一致性读的功能(快照读)。详见官方文档: https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html
定义
mysql有两种读数据的模式,一种是当前读,一种是快照读。 快照读的意思是,数据有多个版本, 当事务并发执行时, 某一事务读取的数据来自其中一个版本(快照)。下面举例说明(Repeatable Read级别):
|时间|事务A|事务B| |--|--|--| |0|set autocommit=0 (开始事务)|set autocommit=0(开始事务)| |1|selcect * from table_a where name='abc' (返回空)|| |2||insert into table_a(name) values('abc')| |3||commit| |4|selcect * from table_a where name='abc' (仍然空) 可以看到,两个并发执行的事务, 即使B插入并提交了数据, A仍然看不到,因为A读的还是快照。
时间点
事务在查询的时候,是查找某一个快照的,那么怎么确定查的是哪一个快照呢? 这个时间点,就是事务第一次查询时的时间,在这个时间点前面提交的数据(其他事务提交),都可以查询到。 像上面那个例子,就是因为事务A第一次查询时间早于事务B提交时间,所以查询不到aaa的数据。下面再看一次例子: |时间|事务A|事务B| |--|--|--| |0|set autocommit=0 (开始事务)|set autocommit=0(开始事务)| |1||insert into table_a(name) values('bbb')| |2||commit| |3|selcect * from table_a where name='bbb' (返回了数据)|| 可以看到事务A和B同时开始,但由于事务A的第一次查询时间晚于B, 所以能查到B提交的数据。 如果在事务中使用了DML更新/删除了其他事务提交的数据,那么这些数据会对当前事务可见,可以查询到,注意仅仅是被影响到的数据,其他在这之前提交的数据一样查询不到。
对插入两条数据问题的解释
来看下可能插入两条数据的流程: |时间|事务A|事务B| |--|--|--| |0|set autocommit=0 (开始事务)|set autocommit=0(开始事务)| |1|Object a = executeQuery("select * from table_a") (返回空)| |2||Object a = executeQuery("select * from table_a") (返回空)| |3||lock 成功| |4|lock 等待| a = executeQuery("select * from table_a")(返回空) |5||executeUpdate("insert into table_a ...") (插入成功)| |6||unlock| |7|lock成功|commit| |8|a = executeQuery("select * from table_a") (由于A的时间点为1,所以查询不到最新数据,返回空)|| |9|executeUpdate("insert into table_a ...") (插入成功) |10|unlock| |11|commit|