前言
前段时间,在 scala 项目的某块业务迁移到微服务上线前,测试环境意外地发生了两次死锁问题,触发死锁的原因不同,一次是因为数据迁移不到位,由更新操作导致,解决方式是重写 update 方法。另一个是因为前人写的代码太过于随意,一个事务中包含了多个模版方法,由不同的子类去实现方法,后人在实现时,有可能并不知道自己实现的方法是被嵌套在事务中的,从而扩展了很多操作,操作中不乏有耗时的远程调用,从而一个小事务被慢慢积累成一个大事务,最终导致了死锁,由于老业务(scala 项目)很复杂,当时解决的方式是将死锁的方法异步处理。从那之后,自己断断续续做了一些关于 MySQL 并发相关的笔记,本文先梳理下 MySQL 控制并发访问的两种手段(MVCC 和几种常用锁)。
本文基于环境:MySQL:5.7、存储引擎:InnoDB、隔离级别:Repeatable read (可重复读)总结的。
# 数据库版本查询方法:
SELECT version();
# 引擎查询方法(建表语句中会显示存储引擎信息,形如:ENGINE=InnoDB):
show create table `表名`;
# 事务隔离级别查询方法:
select @@tx_isolation;
# 事务隔离级别设置方法(设置成可重复读,只对当前Session生效):
set session transaction isolation level read committed;
MVCC 与快照读、当前读
数据库使用锁是为了支持更好的并发,提供数据的完整性和一致性。为了提供更好的并发,InnoDB 提供了非锁定读:不需要等待访问行上的锁释放,读取行的一个快照,该方法是通过 InnoDB 的一个特性:MVCC 来实现的。MVCC 的特点是读不加锁,读写不冲突。InnoDB 利用 undo log 实现了 MVCC。
MVCC,Multi-Version Concurrency Control,多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
在 MVCC 并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
快照读:不加锁的非阻塞读,简单的 select 操作(不包括 select … lock in share mode、select … for update),读取的是记录数据的可见版本(有可能是历史版本),Read Committed 隔离级别下:每次 select 都生成一个快照读。
- select * from table where ?;
当前读:读取的是记录数据的最新版本,并且,当前读返回的记录都会加锁
- select … lock in share mode;
- select … for update;
- insert into table values (…);
- update table set .. where …;
- delete from table where …;
MySQL 中的常用锁

- 乐观锁
乐观锁的实现不需要借助于数据库锁机制,只需要两个步骤:冲突检测和数据更新(CAS(Compare and Swap)),乐观锁的实现一般来说有2种方式:使用版本号、使用时间戳。
- 悲观锁
与乐观锁相对应的就是悲观锁了。悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟 Java 中的 synchronized 关键字很相似,所以悲观锁需要耗费较多的时间,悲观锁由数据库自己实现,使用的时候,我们直接调用数据库的相关语句就可以了。
- 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)
# 事务要获取某些行的 S锁,必须先获得表的 IS锁。
SELECT column FROM table ... LOCK IN SHARE MODE;
- 共享锁【S锁】| 悲观锁的实现
又称读锁,若事务T对数据对象A加上S锁,则T就可以对A进行读取,但不能进行更新(S锁因此又称为读锁),其它事务只能再对数据A加S锁,而不能加X锁(写锁)。即事务T释放A上的S锁之前,所有事务,只能读取A,但不能更新A。
- 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)
# 事务要获取某些行的 X锁,必须先获得表的 IX锁。
SELECT column FROM table ... FOR UPDATE;
- 排他锁【X锁】| 悲观锁的实现
又称写锁,若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其它事务不能再对A加任何锁,直到T释放A上的锁。这保证了其它事务在T释放A上的锁之前不能再读取和修改A。
- 表锁
锁住整张表,特点是开销小、加锁快、锁定粒度大、发生锁冲突的概率最高、并发度最低。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得表锁,这会阻塞其它用户对该表的所有读写操作,故并发性能最低。在服务器上操作诸如 ALTER TABLE
之类的语句时会使用表锁,保证 DDL 对数据不产生影响。
行锁(重点总结)
行锁的特点是开销大、加锁慢、会出现死锁、锁定粒
InnoDB 有三种行锁的算法:
- Record Locks
单个行记录上的锁,锁定一个记录上的索引,而不是记录本身。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
- Gap Locks
间隙锁实质上是对索引前后的间隙上锁,不对索引本身上锁。根据检索条件向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)。间隙锁的目的是为了防止幻读:在加有 Gap 锁期间,防止间隙内有新数据被插入,同时防止已存在的数据,被更新
- N
言七墨 ext-Key Locks
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
总结
本文整理了 MySQL 控制并发访问的两种手段(MVCC 和几种常用锁),主要为我们解决并发死锁的问题奠定一些基础。概括的不是很全面,但是能有一个总体的认识。学无止境,欢迎批评指正。