前言
本文测试和讨论的前提是事务隔离级别为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
的数据