数据库

MySQL 常用锁和 MVCC 总结

言七墨 · 12月10日 · 2019年 · · 236次已读

前言

前段时间,在 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 中,除单个 SQL 组成的事务外,锁是逐步获得的,这就决定了在 InnoDB 中发生死锁是可能的。另外当使用 update 命令更新表数据时,会自动给命中的行加上行锁。另外,MySQL 加行锁时,并不是一次性把所有的行都加上锁,执行一个 update 命令之后,server 层将命令发送给 InnoDB 引擎,InnoDB 引擎找到第一条满足条件的数据,并加锁后返回给 server 层,server 层更新这条数据然后传给 InnoDB 引擎。完成这条数据的更新后,server 层再取下一条数据。 行级锁更适合于有大量按索引并发更新少量不同数据,同时又有并发查询的应用场景。

InnoDB 有三种行锁的算法:

  • Record Locks

单个行记录上的锁,锁定一个记录上的索引,而不是记录本身。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。

  • Gap Locks

间隙锁实质上是对索引前后的间隙上锁,不对索引本身上锁。根据检索条件向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)。间隙锁的目的是为了防止幻读:在加有 Gap 锁期间,防止间隙内有新数据被插入,同时防止已存在的数据,被更新成间隙内的数据。

  • Next-Key Locks

它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。

总结

本文整理了 MySQL 控制并发访问的两种手段(MVCC 和几种常用锁),主要为我们解决并发死锁的问题奠定一些基础。概括的不是很全面,但是能有一个总体的认识。学无止境,欢迎批评指正。

0 条回应