多版本并发控制
多版本并发控制(Multi-Version Concurrency Control, MVCC)以乐观锁为理论基础,和基于锁的并发控制最大的区别和优点是:读不加锁,读写不冲突。
Undo 日志
MVCC 将每一个更新的数据标记一个版本号,在更新时进行版本号的递增,插入时新建一个版本号,同时旧版本数据存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。
快照读与当前读
快照读
快照读只是针对于目标数据的版本号小于等于当前事务的版本号,也就是说读数据的时候可能读到旧数据,但是这种快照读不需要加锁。也就是说,使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。select * from table ...;
当前读
当前读是读取当前数据的最新版本,但是更新等操作会对数据加锁,所以当前读需要获取记录的行锁,存在锁争的问题。以下第一个语句需要加 S 锁,其它都需要加 X 锁。select * from table where ? lock in share mode; # 加 S 锁
select * from table where ? for update;
insert;
update;
delete;
MySQL 中事务隔离级别的实现
1. 可串行化(SERIALIZABLE)
读加共享锁(S),写加排他锁(X),读写互斥。
使用的是悲观锁的理论,实现简单,数据更加安全。
2. 提交读(READ COMMITTED) 和可重复读(REPEATABLE READ)
对于读操作,由于 MVCC 的引入,分为快照读和当前读:
- 快照读:读取的是快照中的数据,不需加锁
- 当前读:读取的是最新的数据,需要加锁
RC 和 RR 都是基于 MVCC 实现的,但是读取的快照数据是不相同的:
- RC 级别下。读取的总是最新的数据。有可能会出现一个事务中两次读到了不同的结果。
- RR 级别下。总是读到小于等于此事务的数据,也就实现了可重复读。
3. 未提交读(READ UNCOMMITTED)
总是读取最新的数据行,无需使用 MVCC。
Next-Key Locks
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题而存在的。
在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。
Record Locks
锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
Gap Locks
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 c 中插入 15。
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
Next-Key Locks
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙,是一个前开后闭区间。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +supremum)
关于 Next-Key Locks 的几个问题
问题一:对主键索引或唯一索引会使用间隙锁吗?
不一定。视情况而定:
- 如果 where 条件全部命中(不会出现幻读),则不会加间隙锁,只会加记录锁
- 如果 where 条件部分命中 / 全都不命中,则会加间隙锁
delete from tb where id = 9
-- Table: tb(name primary key,id unique key)
-- key 是唯一索引
根据 id=9 条件定位,此时给 id = 9 的索引加上记录锁,根据 name 值到主索引中检索获得记录,再给该记录加上记录锁。
问题二:间隙锁是否用在非唯一索引的当前读中?
是的。
delete from tb1 where id = 9
-- Table: tb1(name primary key,id key)
-- key 是非唯一索引
可以看出,在 (6,9] 、(9,11] 加了间隙锁。
问题三:间隙锁是否用在不走索引的当前读中?
是的。
delete from tb2 where id = 9
-- Table: tb2(name primary key,id)
-- 没有为 id 建立索引
此时对所有的间隙都上锁(功能上相当于锁表)。
总结以上三个问题,我们得到如下结论:
- 主键索引 / 唯一索引:
如果 where 条件全部命中(不会出现幻读),则不会加间隙锁,只会加记录锁
如果 where 条件部分命中 / 全都不命中,则会加间隙锁 - 非唯一索引:
会加间隙锁 - 不走索引:
对所有间隙都加间隙锁,相当于锁表