MySQL 锁是数据库为了解决并发事务冲突而设计的机制,核心目的是保证数据在多用户同时访问时的一致性和安全性。锁的类型主要取决于存储引擎,InnoDB 引擎支持的锁最为丰富和复杂 。
锁有哪些主要分类
MySQL 的锁可以从多个维度进行划分,最常用的是按锁的粒度分类 。
- 全局锁
- 定义:锁定整个 MySQL 实例的所有表,加锁后整个数据库只读 。
- 命令:
FLUSH TABLES WITH READ LOCK。 - 场景:全库逻辑备份,确保备份期间数据不被修改 。
- 注意:InnoDB 备份通常使用
--single-transaction实现无锁备份,不推荐使用全局锁 。
- 表级锁
- 定义:每次操作锁住整张表,粒度中等 。
- 类型:
- 表锁:读锁(共享锁)和写锁(排他锁),手动使用
LOCK TABLES加锁 。 - 元数据锁 (MDL):自动加锁,保护表结构,防止表结构被修改时数据不一致 。
- 意向锁:自动加锁,用于协调表级锁与行级锁的冲突检测 。
- 表锁:读锁(共享锁)和写锁(排他锁),手动使用
- 特点:开销小、加锁快,但并发度低,适合读多写少的场景 。
- 行级锁
- 定义:每次操作锁住对应的行数据,粒度最小 。
- 引擎:仅 InnoDB 引擎支持 。
- 类型:
- 记录锁(Record Lock):锁定单条记录,防止 update 和 delete 。
- 间隙锁(Gap Lock):锁定索引记录之间的间隙,不包含记录本身,防止其他事务插入新行 。
- 临键锁(Next-Key Lock):记录锁 + 间隙锁的组合,锁定左开右闭区间,是 InnoDB 默认行锁算法 。
- 特点:并发度高、冲突概率低,但开销大,可能出现死锁 。
行锁和间隙锁怎么工作
行锁是 InnoDB 高并发的核心,其行为受事务隔离级别影响显著 。
- 记录锁的触发
- 当 SQL 语句命中索引时,InnoDB 会锁定索引上的具体记录 。
- 如果 SQL 未命中索引,InnoDB 无法定位具体记录,会对全表所有索引记录加锁,效果等同于表锁 。
- 间隙锁的作用
- 解决幻读:在可重复读 (RR) 隔离级别下,通过锁定间隙防止其他事务在范围内插入新行 。
- 兼容性:多个事务可以同时持有同一个间隙的间隙锁,不会互相阻塞 。
- 互斥性:间隙锁与插入意向锁互斥,会阻塞插入操作 。
- 隔离级别对锁的影响
- 读已提交 (RC):仅存在记录锁,间隙锁关闭,并发性能更高 。
- 可重复读 (RR):行锁 + 间隙锁 + 临键锁完整生效,彻底解决幻读问题 。
- 串行化:所有查询自动加共享锁,所有写操作自动加排他锁,并发性能极差 。
怎么避免死锁和优化锁性能
锁冲突和死锁是高并发场景下的常见问题,可以通过以下策略进行优化 。
- 减少锁持有时间
- 控制事务大小,仅包含核心操作(如锁定数据、更新数据)。
- 非核心操作(如日志记录、通知推送)移到事务外执行 。
- 及时提交或回滚事务,避免长时间未提交 。
- 缩小锁粒度
- 为查询条件字段建立索引,确保 SQL 能命中索引,避免全表扫描加锁 。
- 使用唯一索引的等值查询,使临键锁降级为记录锁,减少锁定范围 。
- 避免执行无 WHERE 条件的 UPDATE/DELETE 语句 。
- 统一锁顺序
- 多个事务操作同一组表/行时,按固定顺序锁定资源,避免循环等待导致死锁 。
- 例如转账场景中,所有事务均按 user_id 升序锁定账户 。
- 合理选择锁策略
- 悲观锁:适用于写多读少、冲突概率高的场景(如银行转账、秒杀库存更新)。
- 乐观锁:适用于读多写少、冲突概率低的场景(如商品浏览量统计)。
- 读写分离:高读并发场景采用主从复制架构,读请求路由到从库 。
- 排查锁问题
- 使用
show processlist查看当前进程与锁等待状态 。 - 使用
show engine innodb status查看死锁日志和锁结构 。 - MySQL 8.0 可使用
performance_schema.data_locks和data_lock_waits精准查询锁资源 。
- 使用
掌握 MySQL 锁机制是保障高并发业务稳定运行的关键,需结合具体业务场景(并发量、冲突概率、一致性要求)灵活选择锁策略 。
锁冲突排查的实操SQL
这是一份针对生产环境的 MySQL 锁冲突与死锁排查实操 SQL 清单。
在 MySQL 8.0+ 环境中,performance_schema和sys库提供了比传统SHOW ENGINE INNODB STATUS更结构化、更易读的视图。以下方案按“发现异常 -> 定位源头 -> 分析原因 -> 紧急处理”的逻辑梳理。
第一阶段:快速感知锁争用
当业务出现接口超时、响应变慢时,先确认是否由锁引起。
1. 查看当前行锁等待概况
-- 关注 innodb_row_lock_current_waits > 0 的情况
SHOW STATUS LIKE 'innodb_row_lock%';
- 关键指标:
Innodb_row_lock_current_waits: 当前正在等待的行锁数量。如果持续大于 0,说明有阻塞。Innodb_row_lock_time_avg: 平均等待时间。如果数值很大,说明持有锁的事务执行很慢或发生了死锁重试。
2. 实时查看谁在等谁(最核心视图)
MySQL 8.0 推荐使用sys.innodb_lock_waits视图,它自动关联了阻塞者和被阻塞者的信息。
SELECT
wait_pid AS waiting_thread_id, -- 被阻塞的线程ID
wait_query AS waiting_sql, -- 被阻塞的SQL语句
block_pid AS blocking_thread_id, -- 阻塞者的线程ID
block_query AS blocking_sql, -- 阻塞者当前持有的SQL(可能为NULL,见下文)
wait_age_secs AS wait_seconds, -- 已等待秒数
locked_table, -- 涉及的表
locked_index -- 涉及的索引
FROM sys.innodb_lock_waits;
- 注意:
blocking_sql可能为NULL。这是因为阻塞事务可能已经执行完了 SQL 语句,但尚未提交(Commit),此时它处于“空闲但持锁”状态。
第二阶段:深度定位“隐形”阻塞者
如果上一步中blocking_sql为空,或者你需要更详细的上下文,需结合performance_schema进行深挖。
3. 查找长事务(常见的锁持有者)
很多锁等待是由一个忘记提交的长事务引起的。
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec, -- 事务运行时长
trx_mysql_thread_id, -- 对应 SHOW PROCESSLIST 的 Id
trx_query -- 当前正在执行的SQL
FROM information_schema.innodb_trx
ORDER BY duration_sec DESC;
- 排查重点:找出
duration_sec很大且trx_state为RUNNING或LOCK WAIT的事务。如果trx_query为 NULL,说明事务处于空闲状态但未提交,它就是潜在的锁持有者。
4. 关联线程获取完整历史 SQL
当阻塞者处于空闲状态时,需要通过performance_schema.events_statements_current或history表找到它最后执行的 SQL。
-- 假设已知阻塞者的线程ID (thread_id) 为 12345
SELECT
THREAD_ID,
EVENT_NAME,
SQL_TEXT,
TIMER_START,
TIMER_END
FROM performance_schema.events_statements_history
WHERE THREAD_ID = 12345
ORDER BY TIMER_START DESC
LIMIT 5; -- 查看该线程最近执行的几条SQL
- 逻辑:通常最后一条
UPDATE/DELETE/SELECT...FOR UPDATE就是加锁的根源。
第三阶段:死锁专项排查
死锁(Deadlock)与锁等待不同,它是循环依赖,InnoDB 会主动回滚其中一个事务。
5. 查看最近一次死锁详情
这是排查死锁最直接的方式,无需开启额外日志。
SHOW ENGINE INNODB STATUS\G
- 阅读技巧:
- 搜索关键字
LATEST DETECTED DEADLOCK。 - 找到
(1) TRANSACTION和(2) TRANSACTION两个块。 - 对比
HOLDS THE LOCK(S)(持有锁)和WAITING FOR THIS LOCK TO BE GRANTED(等待锁)。 - 核心结论:事务 A 持有资源 1 等待资源 2,事务 B 持有资源 2 等待资源 1。
- 记录两个事务最后执行的 SQL,以及它们使用的索引(Index Name)。
- 搜索关键字
6. 开启全量死锁日志(生产环境建议)
默认只记录最后一次死锁,高频死锁场景下容易丢失现场。建议在my.cnf中配置:
[mysqld]
innodb_print_all_deadlocks = 1
log_error = /var/log/mysql/mysqld.log
- 作用:每次发生死锁都会写入错误日志,方便事后回溯分析规律。
第四阶段:紧急处理与解锁
警告:生产环境 Kill 会话需谨慎,务必先确认该会话非核心业务关键路径。
7. 终止阻塞会话
一旦定位到罪魁祸首的线程 ID(blocking_thread_id或trx_mysql_thread_id):
KILL [thread_id];
- 后果:该事务会被回滚,持有的锁立即释放,其他等待的事务得以继续执行。
- 替代方案:如果无法直接 Kill(如权限不足或担心数据一致性),可尝试联系应用层重启服务或等待
innodb_lock_wait_timeout(默认 50s)超时自动回滚。
8. 检查元数据锁(MDL)阻塞
如果SHOW PROCESSLIST中看到大量状态为Waiting for table metadata lock,这通常是因为有人在执行 DDL(如ALTER TABLE)或长事务未提交。
-- 查看 MDL 等待关系
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
LOCK_TYPE,
LOCK_STATUS,
OWNER_THREAD_ID,
REQUESTING_THREAD_ID
FROM performance_schema.metadata_locks
WHERE LOCK_STATUS = 'PENDING';
- 解决:找到持有 MDL 锁的长事务并 Kill 掉,或者等待 DDL 执行完成。
排查总结流程图
- 报警/卡顿 → 查
SHOW STATUS LIKE 'innodb_row_lock%'确认有等待。 - 找关系 → 查
sys.innodb_lock_waits拿到waiting_thread和blocking_thread。 - 找SQL:
- 若
blocking_sql有值 → 直接分析该 SQL 的索引和执行计划。 - 若
blocking_sql为空 → 查information_schema.innodb_trx找长事务 → 查performance_schema.events_statements_history找最后一条 SQL。
- 若
- 定原因:
- 是无索引导致的全表扫描锁?→ 加索引。
- 是间隙锁冲突?→ 调整隔离级别或优化查询条件为唯一索引等值查询。
- 是死锁?→ 查
SHOW ENGINE INNODB STATUS,统一业务层的加锁顺序。
- 解故障 →
KILL阻塞线程或优化代码后重新部署。