MySQL InnoDB 锁机制

MySQL InnoDB引擎锁机制主要有「共享锁Shared Lock」、「排他锁Exclusive Lock」,另外还有「意向锁Intention Lock」,从锁的粒度上来看我们主要关注表锁(Table Lock)、行锁(Row Lock)、以及通过意向锁实现更细粒度的锁。

锁,是操作系统中用于「管理对共享资源的并发访问」的机制。存在「临界资源」的「并发读写访问」时,为保证数据一致性,锁是其中的一种重要手段。

一、不同类型的锁

1.1 共享锁 Shared Lock

S锁,又称为「读锁Read Lock」,共享访问权限,或者说相互不会阻塞。
多个客户端在同一时刻可以同时读取同一个资源,而互不干扰。

1.2 排他锁 Exclusive Lock

X锁,又称为「写锁Write Lock」,特点是排他,一个写锁会阻塞其他的写锁和读锁。
这是出于安全的考虑,只有如此才能保证再给定时间内,只有一个用户执行写入,防止其他用户读取正在写入的统一资源。

SharedLock Vs ExclusiveLock

1.3 意向锁 Intention Lock

上述两种锁对InnoDB而言,都是行锁级别的锁;而更细粒度的,可以允许事务同时在行级、表级加锁。
I锁,意向锁,又分为两类:意向共享锁(IS Lock)、意向排他锁(IX Lock),下面逐一详述。

意向锁,是在表级设置的一种锁,意在表名当前事务将在行级上增加什么类型的锁(S锁或X锁)

  • 意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁。
  • 意向排他锁(IX Lock):事务想要获得一张表中某几行的排他锁。

通过命令show engine innodb status查看存储引擎的锁请求情况,类似如下内容:TABLE LOCK table xxx trx id 10080 lock mode IX

意向锁的加锁规则:

  • 事务在获取行级S锁之前,必须获取其对应表的IS或IX锁
  • 事务在获取行级X锁之前,必须获取其对应表的IX锁

不同锁兼容性

当事务申请锁和表、行已存在的锁兼容时,该锁被授权;否则,则锁等待。意向锁的主要目的是表明:存在请求正在或即将锁定此表的某行。意向锁除了全表请求(例如LOCK TABLES ... WRITE)外,不阻止任何其他内容。

InnoDB 锁相关的表

infomation_schema库中存在三张与锁有关的数据表,分别是:

  • INNODB_TRX:事务信息表,记录事务完整信息
    • 表结构:innodb_trx
    • INNODB_TRX.trx_requested_lock_id:事务等待的锁id,也就是后文中INNODB_LOCKS.lock_id
    • INNODB_TRX.trx_state:事务状态,Lock Wait表示锁等待状态
    • INNODB_TRX.trx_query:事务执行SQL
  • INNODB_LOCKS:锁信息表,记录锁完整信息
    • innodb_locks
    • INNODB_LOCKS.lock_id:锁id
    • INNODB_LOCKS.lock_mode:锁模式,S锁、X锁
    • INNODB_LOCKS.lock_type:锁类型,行锁、表锁
    • INNODB_LOCKS.lock_table:锁定的表
    • INNODB_LOCKS.lock_index/space/page:锁定的索引、spaceId、页
    • INNODB_LOCKS.lock_rec:锁定行的数量
    • INNODB_LOCKS.lock_data:锁定记录的主键值,注意此值为非可信值
  • INNODB_LOCK_WAITS:锁等待信息记录表,记录锁、事务的等待关系,可以直白看出哪些事务发生了锁等待。
    • innodb_lock_waits
    • INNODB_LOCK_WAITS.requesting_trx_id:申请锁资源的事务Id
    • INNODB_LOCK_WAITS.requesting_lock_id:申请的锁Id
    • INNODB_LOCK_WAITS.blocking_trx_id:阻塞的事务Id
    • INNODB_LOCK_WAITS.blocking_lock_id:阻塞的锁Id
    • 也就是说,从lock_waits表可以看出,不同事务的锁等待情况,关联trxlocks表后就可以获取详细的事务、锁信息。

一致性非锁定读 VS 一致性锁定读

  • 一致性非锁定读(consistent nonblocking read):通过「并发多版本控制」MVCC获取当前时间数据库中的行数据。由于获取版本机制的时间点不同,也就产生了不同的隔离级别。如READ COMMITTED总是读取行数据的最新版本,而REPEATABLE READ则是取事务开始时的数据版本。这也就造就了二者的差异,前者多次读取时可取到其他事务已提交的数据,而后者在多次读取时总是保持行为一致。
  • 一致性锁定读(locking read):为保证读操作中数据的一致性,需显式进行加锁来保证并发情况下的数据一致性。
    • select ... for update:在读取行上加X锁,其他事务不能加任何锁
    • select ... in share mode:在读取行上加S锁,其他事务只能加S锁,如果加X锁将导致锁等待
    • 上述两种锁定读操作时,如果其他事务为非锁定读操作时,是可以正常读取的(因为非锁定读操作不会加任何锁)
    • 在进行锁定读操作时,必须显式通过beginstart transactionset autocommit=0将多条SQL放在同一个事务中提交

行锁中算法

  • Record Lock:对单个行记录上锁
    • 通过聚簇索引或二级索引查找时,会在索引上加Record Lock
    • 通过SHOW ENGINE INNODB STATUS可以看到类型如下内容:
1
2
3
4
5
6
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table xxxx
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;
  • Gap Lock:范围锁定
    • 按照范围锁定索引记录
    • 例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;,为防止其他事务将值15插入到t.c1列中,无论该列中是否已有这样的值,该范围中所有现有值之间的间隙都会被锁定。
    • Gap Lock是在性能和并发能力上的一种权衡方案,仅在部分事务隔离级别上采用
    • SELECT * FROM child WHERE id = 100;对这类id具有唯一索引的情况,只会明确加Record锁,而不会加Gap锁
    • 而对于无索引或无唯一索引情况时,会增加Gap锁
  • Next-Key Lock:Gap+Record锁
    • 在唯一索引情况下会降级为RecordLock提高并发能力
    • Next-Key Lock在锁定时会将下一个key区间也进行Gap Lock,目的是为了避免幻读的情况
    • 所谓的Next-Key是指,范围区间划分是包含下一个值
    • 如索引有10,11,13,20四个值时,可被Next-Key Lock的范围区间是(-∞, 10], (10, 11], (11, 13], (13, 20], (20, +∞]五个区间
    • 如插入新值12后,则(10, 11], (11, 13]裂变为(10, 11], (11, 12], (12, 13]

实例1:唯一索引时,Next-Key Lock降级为Record锁,仅锁定单行记录

1
2
3
4
create table t(a int primary key);
insert into t select 1;
insert into t select 2;
insert into t select 5;

此时插入按唯一键a插入时,只会锁定单条记录,如下表:
uniqKey_lock

实例2:辅助索引(非唯一索引或主键索引)

1
2
3
4
5
6
create table z(a int, b int, primary key(a), key(b));
insert into z select 1,1;
insert into z select 3,1;
insert into z select 5,3;
insert into z select 7,6;
insert into z select 10,8;

执行SQLselect * from z where b=3 for update时,锁处理为:

  • 存在两个索引,需分别进行锁定
  • 对于a聚簇索引,对a=5进行RecordLock
  • 对于b辅助索引,按Next-Key对区间(1,3]加锁
  • 同时,InnoDB还会对下一个键值加GapLock,即对区间(3,6]加锁

其他会话的下列SQL将被阻塞(锁等待):

1
2
3
select * from z where a=5 for update; -- a=5,是`a=5`的RecordLock锁定记录,锁等待
insert into z select 4,2; -- b=2,在锁定区间(1,3]范围内,锁等待
insert into z select 6,5; -- b=5 在锁定区间(3,6]范围内,锁等待

二、锁注意问题

  • 脏读(Dirty Read)
  • 不可重复读
  • 丢失更新
  • 幻读(Phantom Problem)
    • Phantom Problem幻读是指,同一个事务中,连续执行两次相同SQL可能出现不同的结果,第二次SQL会返回之前不存在的行。

三、死锁

死锁是指两个或两个以上的事务在执行过程等中,因争夺资源而造成的一种相互等待的现象。

按照《Operating.System.Concepts 操作系统概念》一书中死锁的必要条件:

  • 互斥(Mutual exclusion,简称Mutex)
    • 至少有一个资源处于非共享模式,即一次只能有一个进程使用
    • 如另一进程申请此资源,则须等待该资源释放
  • 占有并等待(Hold and wait)
    • 一个进程必须占有至少一个资源
    • 并等待另一资源
    • 而该资源被其他进程占用
  • 非抢占(No preemption)
    • 资源不能被抢占
    • 即资源只能在进程完成任务后自动释放
  • 循环等待(Circular wait)
    • 一组等待进程{P0, P1…Pn-1, Pn}
    • P0等待资源P1占有,P1等待资源P2占有
    • Pn-1等待资源Pn占有,Pn等待资源P0占有
    • 循环等待,则形成环形结构

死锁预防、检测相关内容可详阅本书,或参考其他文献,本文不再详细展开。

死锁发生的四个条件缺一都无法导致死锁,所以死锁恢复的方法也就是打破其中一项条件,导致死锁解除。

五、参考文献