本篇只介绍 MySQL 锁的基本知识。
我的 MySQL 版本是
MySQL 5.7.34
, 建议使用MySQL 5.6
及之后的版本。
个人理解: 使用锁就是实现事务的原子性与隔离性、数据的一致性。
DDL : 数据定义语言(Data Definition Language), 使用
create
、alter
、drop
来定义数据库的 库、表、列 等, 操作的是数据表结构。
DML : 数据操纵语言(Data Manipulation Language), 使用
select
、insert
、update
、delete
操作数据库中的数据。
DQL : 数据控制语言(Data Query Language), 使用
select
、from
、where
、group by
等查询数据库中的数据。
点我查看 - MySQL - 一文快速了解 MySQL 全局锁、表级锁、行级锁、元数据锁、自增锁、意向锁、共享锁、独占锁、记录锁、间隙锁、临键锁、死锁
点我查看 - MySQL 常用存储引擎简介
点我查看 - MySQL - 可重复读隔离级别在哪种情况下会出现幻读
点我查看 - MySQL 三范式理解
全局锁 就是对整个数据库的表加读锁。加全局读锁后, 增删改、建表、修改表结构 等操作都会被阻塞。
- 备份数据库
- 我也就这个场景实战用过, 当时没有从库且项目是内部使用, 项目启动维护可控, 老板一句话的事儿
- 其它的欢迎各位补充啊
# 需要你连接数据库并使用数据库, 以下为示例
# mysql -u root -p
# use testDatabase;
# 加读锁
flush tables with read lock;
# 解锁
unlock tables;
请看我另一篇文章 MySQL 使用命令行导出导出数据库、数据表、表结构
点我查看 MySQL 官网 - 元数据锁
相信你一定见过这段提示 :
Waiting for table metadata lock
。
MySQL 使用 元数据锁 来管理对数据库对象的并发访问并确保数据一致性。
MySQL5.5
版本起明确 MDL 概念DDL 操作加 MDL写锁
DML 操作加 MDL读锁
不用显示使用, 在访问时系统会自动添加 MDL锁
如果有多个 DDL、DML、DQL 操作语句, 则默认首先满足最高优先级的锁请求
默认情况下, 写锁请求的优先级高于读锁请求
一个一个的获取 元数据锁 语句, 并进行死锁检测
说白话 : 当事务操作开启后, 对数据表进行 DDL、DML、DQL 操作时, 会给数据表加上 MDL 锁, 不可以对数据表结构进行操作, 从而保护数据的一致性。
简单实例截图 : (RR 隔离级别)
点我查看 MySQL 官网 - 自增锁
自增锁是一种特殊的表级锁, 常用情况由插入到具有
AUTO_INCREMENT
列的数据表中的事务使用。
在实际工作当前, 比如
ID
字段为自增字段, 在插入数据时, 就可以不指定ID
的值, 而由系统自动递增赋值, 想一想, 是不是这样。
AUTO_INCREMENT 生成的自动递增锁定模式由
innodb_autoinc_lock_mode
变量进行配置。
innodb_autoinc_lock_mode
有三种设置值, 分别为 0、1、2 。
MySQL 8.0
前, 默认模式为innodb_autoinc_lock_mode=1
MySQL 8.0
起, 默认模式为innodb_autoinc_lock_mode=2
以下自增锁的三种模式的名称与个人理解的知识整理来自 MySQL 官网。
传统锁定模式 :
innodb_autoinc_lock_mode
= 0
理解一 :
向含有
AUTO_INCREMENT
列插入数据时, 为每行记录的自增键赋值自增的值, 并分配一个AUTO-INC
表锁。并且执行完插入语句后就会释放而不是等提交事务后才释放, 确保自动递增的值都是连续的。
理解二 :
- 当 从数据库 复制 主数据库 的 SQL 语句时, 会保持与主数据库 相同的数据, 因为
INSERT
的执行条数与顺序是确定性的。- 再结合官网插入 1000 行的语句实例, 也就是 当预先不确定执行的
INSERT
条数并且需要保证自增的顺序时, 就采用AUTO-INC
表锁。
连续锁定模式 :
innodb_autoinc_lock_mode
= 1
理解一 :
- 当预先不确定执行的
INSERT
条数并且需要保证自增的顺序时, 就采用AUTO-INC
表锁- 当预先确定执行的
INSERT
条数, 会采用 互斥锁(一种轻量级锁)
交错锁定模式 :
innodb_autoinc_lock_mode
= 2
理解一 :
- 不使用
AUTO-INC
表锁- 执行效率在这三种模式之间是最好的
- 当批量插入时, 自动增量值之间可能会存在 间隙或错乱
理解二 :
- 如果在并发的情况下, 并发
INSERT
的执行结果不确定, 则最后生成的自增值可能不是连续的- 在主从数据库中应用时, 该模式无法保证可靠的从主数据库复制
产生序列间隙 和 “丢失”自动增量值
第一种情况(未重启 MySQL服务):
这三种模式中, 如果已生成自增值, 并且事务回滚, 则事务内已生成的自增值将会 丢失且不可被重用。
实例图 :
第二种情况 :
MySQL 5.7 及之前版本
, 事务回滚后并重启 MySQL 服务, 则自增的 ID 值将重新分配给回滚之前最大的自动 ID 值。MySQL 8.0
中, 自增 ID 值将被持久化, 防止重复使用以前分配的值。
意向锁 是 表级锁, 作用是事务即将操作的记录需要哪种类型的锁(共享锁或独占锁)。
意向锁有两种类型 : 意向共享锁(IS) 和 意向独占锁(IX)。
加锁必须保证当前表没有任何锁, 如果每一行都检查, 效率特别低, 这时只需要检查是否存在 意向锁 就行了。
意向共享锁 :
- 表示事务打算在表中记录上设置 共享锁。
- 在事务获取表中记录的 共享锁 之前, 需要先获取数据表中的 意向共享锁或更高级别的锁。
意向独占锁 :
表示事务打算在表中记录上设置 独占锁
在事务获取表中记录的 独占锁 之前, 需要先获取数据表中的 意向独占锁。
如果锁与现有锁兼容, 则将授予事务权限, 如果冲突, 事务阻塞直到释放锁冲突。
锁类型 X 锁 IX 锁 S 锁 IS 锁 X 锁 冲突 冲突 冲突 冲突 IX 锁 冲突 兼容 冲突 兼容 S 锁 冲突 冲突 兼容 兼容 IS 锁 冲突 兼容 兼容 兼容
InnoDB 索引引擎支持行锁与表锁, 前提是所使用的字段必须建立索引, 锁必须打在索引上, 否则会转为表锁。
点我查看 MySQL 官网 - InnoDB 共享锁
共享锁又称读锁, 简称 S 锁。
共享锁在普通查询时不会加锁, 只有在事务当中才会加锁, 加共享锁后, 其他事务只能读, 不能更新删除。
例 : 在事务 A 当中, 查询一行, 产生了 共享锁, 其他事务可以获取同样的 共享锁, 但不能获取 独占锁, 只有 事务 A 释放 共享锁 后事务 B 才可以获得 独占锁。
# 在 select 查询语句末尾加 lock in share mode
# 例:
select name from user where id = 5 lock in share mode;
模式 共享锁 独占锁 共享锁 兼容 互斥 独占锁 互斥 互斥
点我查看 MySQL 官网 - InnoDB 独占锁
- 独占锁又称写锁、排它锁, 简称 S 锁。
- 事务加独占锁后,
R-UC、RC、RR
隔离级别下, 其他事务中可读
、不能更新
, 只能等待释放。- 事务加独占锁后,
Serializable
隔离级别下, 其他事务中不可读
、不能更新
, 只能等待释放。
# 在语句末尾加 for update
# 例:
select name from user where id = 5 for update;
模式 共享锁 独占锁 共享锁 兼容 互斥 独占锁 互斥 互斥
点我查看 MySQL 官网 - InnoDB 记录锁
- 记录锁是对索引记录的锁, 记录锁都是独占锁。
- 如果该记录存在, 即使定义的表没有索引, InnoDB 也会创建一个隐藏的聚集索引并将该索引用于记录锁定, 但如果作用锁的列没有索引, 则进行锁表。
- 点我查看 - MySQL官网 聚集索引
点我查看 MySQL 官网 - InnoDB 间隙锁
只有在可重复读的隔离级别才会有间隙锁!
加间隙锁后, 其他事务不可在该间隙内插入数据, 解决
幻读
的情况。间隙锁可以共存, 事务 A 不会阻止事务 B 在同一间隙上使用间隙锁。
间隙锁是存在于索引记录之间、第一条索引记录之前或最后一条索引记录之后的间隙上的锁。
- 范围查询: 查询某范围内的数据(比如 10 < id < 20)会产生间隙锁。
- 单条查询: 查询单条数据(比如 id = 5), 如果记录存在, 则加记录锁;记录不存在, 则加记录锁与间隙锁。
- 普通索引: 任何情况下都会产生间隙锁
- 唯一索引: 有普通索引时, 优先根据普通索引排序, 再根据唯一索引排序
- 点我查看 - 普通索引与唯一索引结论 出处文章
插入意向锁 :
- 插入意向锁 是在执行插入操作之前设置的一种 间隙锁。
- 即多个事务之间, 数据如果插入到同一索引间隙中但是位置不同, 则不需要相互等待。
- 例如: 有10 和 20 的索引记录(中间无数据)
- 有两个插入 12 和 15 的数据的事务
- 两个事务在获得 X 锁之前, 产生 10 与 20 之间的插入意向锁间隙, 不会发生阻塞
- 因为12 与 15 的行不冲突
点我查看 MySQL 官网 - InnoDB 临键锁
- 临键锁是 记录锁 与 间隙锁 的组合。
点我查看 MySQL 官网 - InnoDB 死锁
- 死锁因 写操作 发生, 当事务之间的锁互相依赖, 都在等待释放时, 就发生了死锁。
MyISAM
不会发生死锁, 因为MyISAM
不支持事务, 当需要获取锁时, 需要等待之前使用的锁释放。但从网上别的文章中看, 当并发量高且处理多张表时, 可能会出现死锁, 本人也没有实验过。InnoDB
支持事务, 业务逻辑复杂时, 很可能发生死锁。
- 第一步: 开启事务 A
- 第二步: 在事务 A 中查询 id = 17 并加共享锁, 此时其它事务只能读取不能修改删除
- 第三步: 开启事务 B
- 第四步: 在事务 B 中删除 id = 17 的数据
- 第五步: 此时不做任何操作, 超时会自动回滚小事务
InnoDB
使用 innodb_lock_wait_timeout 变量, 默认回滚超时为 50 秒。- 只回滚当前语句, 而不是整个事务。
- 要想回滚整个事务, 请点我查看 - innodb_rollback_on_timeout
- 第一步: 开启事务 A
- 第二步: 在事务 A 中查询 id = 17 并加共享锁, 此时其它事务只能读取不能修改删除
- 第三步: 开启事务 B
- 第四步: 在事务 B 中删除 id = 17 的数据
- 因为删除需要 X 锁, S 锁与 X 锁互斥, 所以发生阻塞, 事务 B 需要等事务 A 释放 id = 17 的 S 锁
- 第五步: 在事务 A 中也删除 id = 17 的数据
- 因为删除需要 X 锁, X 锁与 X 锁互斥, 事务 B 此时已经有删除动作, 已产生 X 锁, 事务 A 的删除 X 锁等待事务 B 的删除 X 锁释放, 而事务 B 的 X 锁又等待事务 A 的 S 释放, 此时就产生了死锁。
- 第六步:
InnoDB
检测到死锁, 所以回滚事务当中的小事务, 从而打破死锁。
- 只回滚当前语句, 而不是整个事务。
- 要想回滚整个事务, 请点我查看 - innodb_rollback_on_timeout
- 使用 innodb_deadlock_detect 配置选项禁用死锁检测, 默认是开启状态。
- 启动 死锁检测时, 在高并发时, 多个线程等待同一个锁, 死锁检测 会导致速度非常慢。
- 启动 死锁检测 时,
InnoDB
自动检测事务 死锁 并回滚一个或多个事务从而打破死锁。- 事务的大小从插入、更新或删除的行数决定。
- 减少事务当中不必要的 SQL 操作, 这样事务持续的时间缩短, 减少发生死锁的可能性。
- 在业务支持的情况下, 进行相关更改后立即提交事务, 降低发生死锁的可能性。
- 根据业务实际情况, 使用表级锁会防止并发更新, 操作按顺序执行, 避免死锁。但会降低系统的响应速度。
- 使用 SHOW ENGINE INNODB STATUS 来查看最近的死锁原因。
- 启用 innodb_print_all_deadlocks 收集更广泛的死锁信息, 记录在错误日志当中。
- 当看到死锁原因后, 请调整优化业务逻辑代码, 以避免再次发生死锁。