MySQL事务隔离级别默认是可重复读

多版本并发控制

多版本并发控制(Multi-Version Concurrency Control, MVCC)以乐观锁为理论基础,和基于锁的并发控制最大的区别和优点是:读不加锁,读写不冲突

Undo 日志

MVCC 将每一个更新的数据标记一个版本号,在更新时进行版本号的递增,插入时新建一个版本号,同时旧版本数据存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。

image.png

快照读与当前读

  • 快照读
    快照读只是针对于目标数据的版本号小于等于当前事务的版本号,也就是说读数据的时候可能读到旧数据,但是这种快照读不需要加锁。也就是说,使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。

    1. select * from table ...;
  • 当前读
    当前读是读取当前数据的最新版本,但是更新等操作会对数据加锁,所以当前读需要获取记录的行锁,存在锁争的问题。以下第一个语句需要加 S 锁,其它都需要加 X 锁。

    1. select * from table where ? lock in share mode; # 加 S 锁
    2. select * from table where ? for update;
    3. insert;
    4. update;
    5. 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。

  1. SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙,是一个前开后闭区间。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:

  1. (-∞, 10]
  2. (10, 11]
  3. (11, 13]
  4. (13, 20]
  5. (20, +supremum)

关于 Next-Key Locks 的几个问题

问题一:对主键索引或唯一索引会使用间隙锁吗?

不一定。视情况而定:

  • 如果 where 条件全部命中(不会出现幻读),则不会加间隙锁,只会加记录锁
  • 如果 where 条件部分命中 / 全都不命中,则会加间隙锁
  1. delete from tb where id = 9
  2. -- Table: tb(name primary key,id unique key)
  3. -- key 是唯一索引

根据 id=9 条件定位,此时给 id = 9 的索引加上记录锁,根据 name 值到主索引中检索获得记录,再给该记录加上记录锁。

image.png

问题二:间隙锁是否用在非唯一索引的当前读中?

是的。

  1. delete from tb1 where id = 9
  2. -- Table: tb1(name primary key,id key)
  3. -- key 是非唯一索引

image.png

可以看出,在 (6,9] 、(9,11] 加了间隙锁。

问题三:间隙锁是否用在不走索引的当前读中?

是的。

  1. delete from tb2 where id = 9
  2. -- Table: tb2(name primary key,id)
  3. -- 没有为 id 建立索引

image.png

此时对所有的间隙都上锁(功能上相当于锁表)。

总结以上三个问题,我们得到如下结论:

  • 主键索引 / 唯一索引:
    如果 where 条件全部命中(不会出现幻读),则不会加间隙锁,只会加记录锁
    如果 where 条件部分命中 / 全都不命中,则会加间隙锁
  • 非唯一索引:
    会加间隙锁
  • 不走索引:

对所有间隙都加间隙锁,相当于锁表

参考资料