← Back to list

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|