原文地址: https://blog.lvcc.top/article-detail?articleId=351
问题引入
有次小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 |