前言

本文测试和讨论的前提是事务隔离级别为REPEATABLE READ(默认)且存储引擎为InnoDB的场景

测试表结构

CREATE TABLE `members` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Consistent Nonlocking Reads(一致性非锁定读,快照读)

官方定义

常见场景 使用SELECT查询

time transaction1 transaction2
T1 BEGIN BEGIN
T2 SELECT * FROM members  
T3   INSERT INTO members (name) VALUES (‘demo’)
T4   COMMIT
T5 SELECT * FROM members  

以上示例中T2和T5读取数据一致,原因是T5读取的是当前事务开启后T2创建的快照(事务执行过程中的第一次读取)

time transaction1 transaction2
T1 BEGIN BEGIN
T2   INSERT INTO members (name) VALUES (‘demo’)
T3   COMMIT
T4 SELECT * FROM members  

以上示例中T4能够读取到transaction2的已提交数据,因为T4是事务中第一次读取,此时才会创建快照,由于transaction2数据在T3时间已提交,早于T4,因此T4可读取到

time transaction1 transaction2 transaction3
T1 BEGIN BEGIN BEGIN
T2   INSERT INTO members (name) VALUES (‘demo’)  
T3   COMMIT INSERT INTO members (name) VALUES (‘dm’)
T4 SELECT * FROM members    
T5     COMMIT
T6 SELECT * FROM members    

以上示例中T4能够读取到transaction2的已提交数据,但T6无法读取到transaction3在T5提交的数据,原因同上

Locking Reads(锁定读取,当前读)

官方定义

几种常见的场景

  • SELECT…LOCK IN SHARE MODE
  • SELECT…FOR UPDATE
  • UPDATE/DELETE/INSERT

与快照读对比测试

time transaction1 transaction2
T1 BEGIN BEGIN
T2 SELECT * FROM members  
T3   INSERT INTO members (name) VALUES (‘demo’)
T4   COMMIT
T5 SELECT * FROM members  
T6 SELECT * FROM members for update  
T7 SELECT * FROM members lock in share mode  

以上示例中由于transaction1在T2创建了快照,因此T5的查询结果跟T2一致,但T6,T7的查询使用了排他锁和共享锁,因此触发了锁定读取(当前读),获取到最新结果

相关应用

当前有一个商品库存为1,假设同时有2个请求到达服务器,每个请求购买数量为1,程序判断如下:

-- 判断库存是否足够(由于是并发请求,所以2个请求都通过判断)
-- BEGIN
-- 更新商品库存为`当前库存`-1并获取更新结果(UPDATE `商品表` SET `库存`=`库存`-1 WHERE `库存`>1)
-- 上一步执行成功:继续执行其他订单相关业务流程/错误:返回
-- COMMIT

通过WHERE条件进行Locking Reads,获取UPDATE操作时间点最新数据,避免可能超卖的情况发生

总结

InnoDB使用多版本并发控制(mvcc)来为数据库在某一个时间点的数据呈现快照. 例如:事务A开始了事务,但快照的时间点需要在事务A进行第一次SELECT查询的时候生成,生成之后,后续在事务ACOMMIT/ROLLBACK之前的所有查询的数据都将小于等于这个快照创建的时间点,只有触发了当前读的查询才可以读取到其他事务已COMMIT的数据