SQL准备
主键id,索引c。
- 下面的语句怎么加锁,何时释放?
- 该语句会命中d=5一行,对应主键id=5。
因此在select 语句执行完后,id=5一行会加写锁。因两阶段锁协议,写锁会在执行commit语句时释放。
由于字段d无索引,该查询语句会全表扫描。其他被扫到但不满足条件的5行记录会不会被加锁呢?
InnoDB默认事务隔离级别可重复读。
幻读
若只在id=5一行加锁,而其他行不加锁:
- 假设只在id=5一行加行锁
- session A执行三次查询-Q1、Q2和Q3,SQL语句相同:查所有d=5的行,且使用当前读并加写锁。
Q1只返回id=5一行
T2时,session B把id=0一行的d值改成5,因此T3时Q2查出来的是id=0和id=5这两行
T4时,session C插入(1,1,5),因此T5时Q3查出来的是id=0、id=1和id=5的三行
Q3读到id=1这一行称为“幻读”,即一个事务在前后两次查询同一范围时,后一次查询看到前一次查询没看到的行。
在可重复读下,普通查询是快照读,不会看到别的事务插入的数据。因此,幻读在“当前读”下才会出现。
session B的修改结果,被session A之后的select语句用“当前读”看到,不能称为幻读。幻读仅专指新插入的行。
这三查询都加了for update,都是当前读。当前读就是要能读到所有已提交的记录的最新值。
session B和sessionC的两条语句,执行后就会提交,所以Q2和Q3就应该看到这俩事务的操作效果,所以这和事务的可见性不矛盾。
但这真的没问题?
不,这里还真有问题。
幻读的问题
语义问题
session A在T1时刻就声明了,“我要把所有d=5的行锁住,不准别的事务进行读写操作”。而实际上,这个语义被破坏了。
再往session B和session C里面分别加一条SQL语句,你再看看会出现什么现象。
- 假设只在id=5这行加行锁 - 语义被破坏
- session B的第二条语句update t set c=5 where id=0,由于在T1,session A 还只是给id=5这行加行锁, 并未给id=0这行加锁。
因此,session B在T2,可执行这两条update。这就破坏了 session A 里Q1语句要锁住所有d=5的行的加锁声明。
同理,session C对id=1这行的修改,也是破坏了Q1的加锁声明。
数据一致性问题
锁是为了保证数据一致性。而这个一致性,不止是DB内部数据状态在此刻的一致性,还包含数据和日志在逻辑上的一致性。
我给session A在T1时刻再加一个更新语句,即:update t set d=100 where d=5。
- 假设只在id=5这一行加行锁 - 数据一致性问题
- update的加锁语义和select …for update 一致,所以这时加上这条update语句也很合理。session A声明说“要给d=5的语句加锁”,就是为更新数据,新加的这条update语句就是把它认为加上锁的这行d值修改成了100。
以上序列执行完成后,DB结果是啥呢?
T1后,id=5这行变成 (5,5,100),该结果最终在T6时刻正式提交
T2后,id=0这行变成(0,5,5)
T4后,表里多了行(1,5,5)
其他行跟该执行序列无关,保持不变。
这样看,这些数据也没啥问题,但再看binlog:
T2,session B事务提交,写入两条语句
T4,session C事务提交,写入两条语句
T6,session A事务提交,写入update t set d=100 where d=5 语句。
放到一起:
update t set d=5 where id=0; /*(0,0,5)*/ update t set c=5 where id=0; /*(0,5,5)*/ insert into t values(1,1,5); /*(1,1,5)*/ update t set c=5 where id=1; /*(1,5,5)*/ update t set d=100 where d=5;/*所有d=5的行,d改成100*/
这个语句序列,不论是拿到备库去执行,还是以后用binlog来克隆一个库,这三行结果都变成了 (0,5,100)、(1,5,100)和(5,5,100)。
即id=0和id=1这两行,发生数据不一致!