Mysql 锁
Mysql 锁
Mysql 锁分类:
- 根据加锁的范围锁
- 全局锁
- 表级锁
- 行级锁
- 根据锁的互斥性
- 共享锁/S 锁
- 排他锁/X 锁
- 共享排他锁/SX 锁
- 根据操作类型
- 读锁:查询数据时使用的锁
- 写锁:执行插入、删除、修改、
DDL语句时使用的锁
- 根据加锁方式
- 显示锁:编写
SQL语句时,手动指定加锁的粒度 - 隐式锁:执行
SQL语句时,根据隔离级别自动为SQL操作加锁
- 显示锁:编写
共享锁的不排斥仅针对不同事务之间读读共享
表锁和行锁满足读读共享、读写互斥、写写互斥
全局锁
全局加锁
flush tables with read lock
该sql指令将数据库设为只读状态,任何其他线程执行的对数据的增删改操作以及对表结构的更改操作都被阻塞
释放全局锁
unlock tables
全局锁主要用于全库逻辑备份,备份时间过长会造成业务停滞
不使用全局锁,创建事务在可重复读隔离级别下通过快照进行备份可避免该问题
表级锁
表锁
//表级别的共享锁,也就是读锁;
lock tables tablename read;
//表级别的排他锁,也就是写锁;
lock tables tablename write;
表锁同时限制本线程和其他线程对表的读写
尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能
元数据锁 MDL
MDL 锁被 Mysql 隐式使用
- 对一张表进行 CRUD 操作时,加 MDL 读锁
- 对一张表做结构变更操作的时候,加 MDL 写锁
MDL读锁
- 查询表结构时自动添加
- 执行查询语句时如需扫描表则自动添加
- 执行存储过程时如访问某表则自动添加
MDL写锁 - 修改表结构时自动添加
- 执行写操作时如需扫描表则自动添加
- 执行存储过程时如修改某表则自动添加
MDL 锁在事务提交后被释放,且 MDL 写锁获取优先级高于读锁
当长事务 MDL 读锁堵塞某 MDL 写锁时,之后的所有 MDL 读锁都被堵塞
意向锁
- 在使用 InnoDB 引擎的表里对某些记录加上共享锁之前,需要先在表级别加上一个意向共享锁
- 在使用 InnoDB 引擎的表里对某些记录加上排他锁之前,需要先在表级别加上一个意向排他锁
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁( 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]:
插入意向锁
插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作
当执行插入操作时,总会检查当前插入位置的下一条行记录(已存在的主索引节点)上是否存在间隙锁对象,判断是否间隙被锁定,如果锁住了,则判定和插入意向锁冲突,当前插入操作被阻塞
插入意向锁配合间隙锁或临键锁一起防止了幻读操作
插入意向锁之间不会互相冲突,多个插入操作同时插入同一个间隙时无需互相等待
MySQL 加锁时,先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁
隐式锁
Insert 语句正常执行时不生成锁结构,靠聚簇(主键)索引记录自带的 trx_id 隐藏列作为隐式锁来保护记录
当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB 会跳过加锁环节,这种机制称为隐式锁
隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能
隐式锁只有在特殊情况下才会转换为显示锁,例如在并发插入数据的时候发生了主键索引冲突
- 主键索引冲突:先插入记录上的隐式锁变为X 型记录锁,后插入新记录的事务尝试给记录添加S 型记录锁而阻塞
- 唯一索引冲突:先插入记录上的隐式锁变为X 型记录锁,后插入新记录的事务尝试给记录添加S 型临键锁而阻塞
行级加锁
行级锁加锁的对象是索引
锁的基本单位是临键锁,但在仅使用记录锁或间隙锁就能避免幻读的情况下,临键锁退化为记录锁或间隙锁
查看加锁情况
select * from performance_schema.data_locks\G;
没有使用索引的锁定读语句/update 语句/delete 语句会导致全表扫描,对每一条记录都加临键锁
mysql 的行级别锁的锁结构是在内存中的,并不会持久化到磁盘,因为事务提交就会释放锁,即使事务中途没执行完挂掉了,也会回滚事务,所以没必要持久化磁盘
锁结构比较关键的信息主要有事务 id,锁的状态,记录的 id
等值查询
唯一索引:
- 索引上的等值查询,在给唯一索引加锁时,临键锁会退化为记录锁,因为主键是唯一的
- 索引上的等值查询, 继续向右遍历时且最后一个值不满足等值条件的时候,临键锁退化为间隙锁
非唯一索引:
- 非唯一索引等值查询的过程是一个扫描的过程,对于二级索引加锁扫描二级索引的 B+树(按照二级索引对记录进行排序),扫描到第一条不符合条件的二级索引记录就停止扫描
- 当查询的记录「存在」时,由于不是唯一索引,所以可能存在索引值相同的记录
- 扫描的过程中对符合条件的二级索引记录加的是临键锁
- 在符合查询条件的记录的主键索引上加记录锁
- 对于第一个不符合条件的二级索引记录,该二级索引的临键锁会退化成间隙锁
- 当查询的记录「不存在」时
- 第一条不符合条件的二级索引记录的临键锁会退化成间隙锁
- 因为不存在满足查询条件的记录,所以不会对主键索引加锁
范围查询
唯一索引:
- 针对
>=的范围查询,等值查询对应的索引如存在,其上的临键锁退化为记录锁 - 针对
<的范围查询,等值查询对应的索引如存在,其上的临键锁退化为间隙锁
非唯一索引范围查询过程中索引的临键锁不会退化为间隙锁和记录锁
普通查询
普通的 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必须满足以下条件之一才能执行成功:
- 使用 where,并且 where 条件中必须有索引列
- 使用 limit;
- 同时使用 where 和 limit,此时 where 条件中可以没有索引列
delete 语句必须满足以下条件能执行成功: - 同时使用 where 和 limit,此时 where 条件中可以没有索引列
乐观锁
乐观锁即是无锁思想
在 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有两种策略通过打破循环等待条件来解除死锁状态:
- 设置事务等待锁的超时时间,当一个事务的等待时间超过该值后进行回滚释放锁,InnoDB 中通过参数
innodb_lock_wait_timeout设置超时时间,默认值为 50s - 开启主动死锁检测,主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行,通过参数
innodb_deadlock_detect设置,默认为 on
