Mysql 锁

Mysql 锁

Mysql 锁分类:

共享锁的不排斥仅针对不同事务之间读读共享
表锁和行锁满足读读共享、读写互斥、写写互斥

全局锁

全局加锁

flush tables with read lock

该sql指令将数据库设为只读状态,任何其他线程执行的对数据的增删改操作以及对表结构的更改操作都被阻塞

释放全局锁

unlock tables

全局锁主要用于全库逻辑备份,备份时间过长会造成业务停滞
不使用全局锁,创建事务在可重复读隔离级别下通过快照进行备份可避免该问题

表级锁

表锁

//表级别的共享锁,也就是读锁;
lock tables tablename read;
//表级别的排他锁,也就是写锁;
lock tables tablename write;

表锁同时限制本线程和其他线程对表的读写
尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能

元数据锁 MDL

MDL 锁被 Mysql 隐式使用

MDL读锁

MDL 锁在事务提交后被释放,且 MDL 写锁获取优先级高于读锁
当长事务 MDL 读锁堵塞某 MDL 写锁时,之后的所有 MDL 读锁都被堵塞

意向锁

意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁( lock tables … read )和独占表锁( lock tables … write )发生冲突

意向锁的目的是快速判断表里是否有记录被加锁

AUTO-INC锁

AUTO-INC 锁用于保护表内的自增值,在插入数据时先为表加一个 AUTO-INC 锁再为被 AUTO_INCREMENT 修饰的字段赋递增值,以保证表中自增值连续递增

AUTO-INC锁采用特殊表锁机制,在一个SQL语句执行完后立刻释放,而不是在事务结束时释放

V5.1.22后 InnoDB 提供了一种轻量级新锁代替 AUTO-INC 锁,在插入数据时为自增字段加锁,赋值后立刻解锁

InnoDB 通过 innodb_autoinc_lock_mode 变量设置使用锁
但可能发生主从不一致问题

行级锁

InnoDB 引擎支持行级锁而 MyISAM 引擎并不支持

InnoDB 中行锁要等到事务结束时才释放,不显式开启事务时,InnoDB 为每条语句默认开启事务

Record Lock 记录锁

Record Lock分为S锁和X锁,S锁相当于读锁,X锁相当于写锁

Gap Lock 间隙锁

Gap Lock只存在于可重复读隔离级别,为范围加锁,范围为开开区间

间隙锁也分为S锁和X锁,但间隙锁之间都相互兼容,不存在互斥关系

锁定区间:根据检索条件向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B)

Next-Key Lock 临键锁

临键锁是记录锁和间隙锁的组合,锁定索引本身以及索引之前的间隙,相当于左开右闭区间范围锁

锁定区间(A,B]:

64715c80d9b748d5b9208f244706040a.png (927×315) (alicdn.com)

插入意向锁

插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作

当执行插入操作时,总会检查当前插入位置的下一条行记录(已存在的主索引节点)上是否存在间隙锁对象,判断是否间隙被锁定,如果锁住了,则判定和插入意向锁冲突,当前插入操作被阻塞

插入意向锁配合间隙锁或临键锁一起防止了幻读操作

插入意向锁之间不会互相冲突,多个插入操作同时插入同一个间隙时无需互相等待

Note

MySQL 加锁时,先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁

隐式锁

Insert 语句正常执行时不生成锁结构,靠聚簇(主键)索引记录自带的 trx_id 隐藏列作为隐式锁来保护记录

当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB 会跳过加锁环节,这种机制称为隐式锁
隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能

隐式锁只有在特殊情况下才会转换为显示锁,例如在并发插入数据的时候发生了主键索引冲突

行级加锁

行级锁加锁的对象是索引
锁的基本单位是临键锁,但在仅使用记录锁或间隙锁就能避免幻读的情况下,临键锁退化为记录锁或间隙锁

查看加锁情况

select * from performance_schema.data_locks\G;

没有使用索引的锁定读语句/update 语句/delete 语句会导致全表扫描,对每一条记录都加临键锁

mysql 的行级别锁的锁结构是在内存中的,并不会持久化到磁盘,因为事务提交就会释放锁,即使事务中途没执行完挂掉了,也会回滚事务,所以没必要持久化磁盘
锁结构比较关键的信息主要有事务 id,锁的状态,记录的 id

等值查询

唯一索引:

非唯一索引:

范围查询

唯一索引:

非唯一索引范围查询过程中索引的临键锁不会退化为间隙锁记录锁

普通查询

普通的 select 语句不对记录加锁,属于快照读

在查询时加锁的语句称为锁定读

-- 对读取的记录加共享锁(S型锁)
select ... lock in share mode;
-- 对读取的记录加排他锁(X型锁)
select ... for update;

更新

update 和 delete操作都会加行级排他锁

-- 对操作的记录加排他锁(X型锁)
update table .... where id = 1;
-- 对操作的记录加排他锁(X型锁)
delete from table where id = 1;

阻塞

插入语句在插入一条记录之前,先定位到该记录在 B+树 的位置,如果插入的位置的下一条记录的索引上有间隙锁,才会发生阻塞

对于update语句的全局加锁,可将 MySQL 里的 sql_safe_updates 参数设置为 1,开启安全更新模式
该模式下update必须满足以下条件之一才能执行成功:

乐观锁

乐观锁即是无锁思想

在 MySQL 中可以通过 version 版本号 + CAS 的形式实现乐观锁,也就是在表中多设计一个 version 字段,然后在 SQL 修改时以如下形式操作:

begin;
select nums, version from tb where goods_id = {$goods_id};
UPDATE ... SET version = version + 1 ... WHERE ... AND version = version;

也就是每条修改的SQL都在修改后对version字段加一,比如T1、T2两个事务一起并发执行时,当T2事务执行成功提交后,就会对version+1,因此事务T1的version=version条件就无法成立,最终会放弃执行,因为已经被其他事务修改过了

当然,一般的乐观锁都会配合轮询重试机制,比如上述T1执行失败后,再次执行相同语句,直到成功为止。

从上述过程中不难看出,这个过程中确实未曾添加锁,因此也做到了乐观锁 / 无锁的概念落地,但这种形式却并不适合所有情况,比如写操作的并发较高时,就容易导致一个事务长时间一直在重试执行,从而导致客户端的响应尤为缓慢。

因此乐观锁更加适用于读大于写的业务场景,频繁写库的业务则并不适合加乐观锁。

死锁

!../4.OperatingSystem/OS.0c1.锁#死锁条件

可以使用 select * from performance_schema.data_locks\G 语句查看 Mysql 加锁情况

从死锁的定义来看,MySQL 出现死锁的几个要素为:

Mysql有两种策略通过打破循环等待条件来解除死锁状态:

mysql死锁场景整理 - 简书 (jianshu.com)