前言

本文测试和讨论的前提是事务隔离级别为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  
T2   BEGIN
T3 SELECT * FROM members  
T4   INSERT INTO members (name) VALUES (‘demo’)
T5   COMMIT
T6 SELECT * FROM members  

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

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

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

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

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

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

官方定义

几种常见的场景

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

与快照读对比测试

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

以上示例中由于transaction1在T3创建了快照,因此T6的查询结果跟T3一致,但T7,T8的查询使用了排他锁和共享锁,因此触发了锁定读取

相关应用

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

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

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

总结

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