ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL死锁排查与解决:Java后端面试实战指南

MySQL死锁排查与解决:Java后端面试实战指南 很多年没更新博客了后台经常收到读者私信问一些面试题和线上问题排查思路。最近一位读者问了个很有意思的问题“线上数据库死锁了运维直接把服务重启了这样对吗如果面试官问‘MySQL 死锁怎么办’每次回答‘重启服务’会被认为没经验吗”说实话这个问题在 Java 后端面试中几乎是必问的而且非常能区分候选人的实战水平。如果只是机械地回答“重启服务”面试官基本可以判断你没有真正处理过线上数据库问题。数据库死锁不是不可恢复的故障而是 InnoDB 存储引擎在并发事务冲突时的一种“自我仲裁”机制。真正的问题在于你知不知道死锁是怎么产生的能不能快速定位到死锁的 SQL能不能通过合理的索引设计、事务拆分、锁顺序管理来避免死锁本文围绕“Java 面试 MySQL 死锁排查与解决”这条主线从底层原理、真实场景、排查命令、Java 侧代码优化几个维度展开。全文都是基于实际项目和常见线上案例整理的完整笔记新手可以理解事务与锁的关系有经验的开发者可以直接参考排查 SQL 和优化方案。内容包括四种死锁场景复现、SHOW ENGINE INNODB STATUS日志怎么看、information_schema 三张表如何定位锁、Java 代码如何通过超时控制和锁顺序来兜底。1. 死锁是什么先搞懂 InnoDB 锁机制很多 Java 开发同学对“死锁”的理解停留在大学操作系统课本上的概念“两个线程各持有一把锁互相等待对方释放锁导致程序永远卡住。”这个理解是对的但不够具体。MySQL 里的死锁远比教科书例子复杂因为它涉及行锁、间隙锁、插入意向锁、自增锁、唯一键冲突等一堆概念。1.1 InnoDB 锁的类型MySQL 默认存储引擎是 InnoDB它支持事务和行级锁。行级锁听起来很细粒度、很好用但也正因为粒度细才容易出现多个事务在“不同行”上加锁之后互相等待。从锁模式上看InnoDB 有三类锁锁模式含义兼容性共享锁S锁允许持有事务读取一行数据S 与 S 兼容S 与 X 互斥排他锁X锁允许持有事务更新或删除一行数据X 与 S、X 都与 X 互斥意向锁IS/IX表级标记表示事务准备在表中某行加 S/X 锁意向锁之间互相兼容如果面试官问“MySQL 的锁有哪些”你只说出共享锁和排他锁是不够的。还要补充意向锁的存在意义意向锁是为了快速判断表级操作如LOCK TABLES、ALTER TABLE和行级锁是否冲突避免逐行扫描检查。从锁的粒度来看还要区分记录锁、间隙锁、临键锁记录锁Record Lock锁住索引记录本身。间隙锁Gap Lock锁住索引记录之间的间隙防止其他事务在该间隙插入数据。临键锁Next-Key Lock记录锁和间隙锁的组合锁住一个范围及其左开右闭区间。这里需要特别提醒如果 InnoDB 表的查询没有走索引行锁会升级为表锁死锁和锁等待的概率会急剧上升。1.2 死锁产生的四个必要条件回到最基础的概念死锁产生需要同时满足四个条件互斥资源同一时刻只能被一个事务持有。持有并等待事务已经持有一个锁同时在等待另一个锁。不可剥夺已持有的锁不能被其他事务强制拿走。循环等待两个或多个事务形成闭环等待链。MySQL 的 InnoDB 引擎会自动检测死锁。当检测到死锁时会回滚其中一个事务通常是代价较小的事务并返回错误码1213和错误信息Deadlock found when trying to get lock; try restarting transaction。所以“死锁”在 MySQL 里并不等同于“卡死”或“服务不可用”。InnoDB 通过锁等待超时和死锁检测机制保证数据库不会因为死锁而彻底崩溃。真正让系统变慢的往往不是死锁本身而是大量等待中的事务堆积、连接池耗尽、慢 SQL 放大效应。1.3 为什么“重启服务”不能解决死锁重启服务在极少数情况下能临时缓解现象因为服务重启后数据库连接池中断未提交的事务会被回滚锁自然释放。但问题在于线上服务重启会导致请求中断业务受损。死锁发生的根因SQL 设计、索引缺失、事务过长、锁顺序不一致完全没有被解决。重启后同一个流量模型下死锁会再次出现。如果是多个应用实例连接同一个数据库重启一个服务实例并不能释放其他实例持有的事务锁。面试时你直接回答“重启服务”等于向面试官传递两个信号一是你没有系统性排查过数据库锁问题二是你对事务、锁、死锁检测这些机制理解不深。正确的思路应该是先定位死锁 SQL → 分析事务加锁顺序 → 设计合理的索引和事务边界 → 通过代码兜底重试机制 → 监控和预警。2. 环境准备与常用排查命令在正式分析死锁场景之前先准备一套可以复现的实验环境。下面的环境和配置适用于本地开发测试生产环境的版本需要根据实际项目确认本文重点是排查思路不绑定特定版本。2.1 推荐实验环境建议使用以下组合MySQL 8.0 或 5.7 版本均可本文示例 SQL 在这两个版本通用。客户端工具命令行mysql客户端或 MySQL Workbench、Navicat。Java 侧示例基于 Spring Boot 2.x / 3.xJDBC 驱动mysql-connector-j版本需要与 MySQL 服务端版本匹配。为了观察锁等待可以将事务隔离级别设置为REPEATABLE READ这是 MySQL 默认隔离级别。创建一张简单的用户账户表用于演示常见的行锁死锁场景。-- 示例表用户账户 CREATE TABLE t_user_account ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, user_code VARCHAR(32) NOT NULL COMMENT 用户编号, balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 账户余额, status TINYINT NOT NULL DEFAULT 1 COMMENT 账户状态 1-正常 2-冻结, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_user_code (user_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户账户表;插入几条测试数据。INSERT INTO t_user_account (id, user_code, balance) VALUES (1, U1001, 100.00), (2, U1002, 100.00), (3, U1003, 100.00);2.2 查看死锁的核心命令排查 MySQL 死锁第一个想到的命令是SHOW ENGINE INNODB STATUS;这个命令会返回 InnoDB 引擎的完整状态信息其中LATEST DETECTED DEADLOCK部分记录了最近一次死锁的详情包括死锁发生的时间。涉及的事务 ID。每条事务当前持有和等待的锁。执行的 SQL 语句。被回滚的事务。执行结果中死锁部分大致长这样------------------------ LATEST DETECTED DEADLOCK ------------------------ 2024-01-15 10:23:45 *** (1) TRANSACTION: TRANSACTION 10086, ACTIVE 3 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 28 page no 4 n bits 72 index uk_user_code of table test.t_user_account *** (2) TRANSACTION: TRANSACTION 10087, ACTIVE 2 sec starting index read mysql tables in use 1, locked 1 2 lock struct(s), heap size 1136, 1 row lock(s) *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 28 page no 4 n bits 72 index uk_user_code of table test.t_user_account *** WE ROLL BACK TRANSACTION (2)另外两个非常有用的元数据表在information_schema库中-- 查看当前所有事务 SELECT * FROM information_schema.INNODB_TRX; -- 查看当前锁等待 SELECT * FROM information_schema.INNODB_LOCK_WAITS;当线上出现锁等待或死锁时可以综合查询事务和锁等待关系快速确认哪个事务阻塞了哪个事务。不过随着 MySQL 版本升级部分字段名会有调整执行前可以先DESC一下表结构。-- 更直观地查询阻塞关系 SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.INNODB_TRX WHERE trx_state RUNNING;3. 线上最常见的死锁场景与复现死锁不是凭空产生的它背后一定有一条具体的业务链路。下面从 Java 后端开发最常见的四个场景出发逐个复现并分析加锁顺序。3.1 场景一两个事务以不同顺序更新同一批数据这是最经典、最典型的一个死锁场景。假设有两条业务逻辑业务 A先更新用户 U1001 的余额再更新用户 U1002 的余额。业务 B先更新用户 U1002 的余额再更新用户 U1001 的余额。当业务 A 和业务 B 并发执行时事务 A 持有 U1001 的行锁等待 U1002 的行锁事务 B 持有 U1002 的行锁等待 U1001 的行锁。循环等待形成死锁。打开两个终端窗口模拟两个事务事务 1BEGIN; UPDATE t_user_account SET balance balance - 10 WHERE user_code U1001; -- 模拟停顿不要执行 COMMIT -- 此时事务 1 持有 user_code U1001 这一行的 X 锁 -- 然后尝试更新 U1002 UPDATE t_user_account SET balance balance - 10 WHERE user_code U1002; COMMIT;事务 2BEGIN; UPDATE t_user_account SET balance balance - 10 WHERE user_code U1002; -- 模拟停顿 -- 此时事务 2 持有 user_code U1002 这一行的 X 锁 -- 然后尝试更新 U1001 UPDATE t_user_account SET balance balance - 10 WHERE user_code U1001; COMMIT;需要按顺序执行先开事务 1执行第一条 UPDATE再开事务 2执行第一条 UPDATE此时两个事务各持有一行锁。然后事务 1 尝试更新 U1002事务 2 尝试更新 U1001。InnoDB 死锁检测机制会在片刻之后检测到循环等待一方被回滚。根因分析两个事务对多个资源的加锁顺序不一致导致循环等待。解决办法在业务代码中约定统一的数据更新顺序。例如先按user_code升序排序再按顺序更新。所有线程都按同一个顺序加锁就不会形成闭环。如果批量更新数据可以在 Java 中先List.sort()再逐条更新能显著降低死锁概率。3.2 场景二唯一键冲突引发的死锁唯一键冲突导致的死锁非常隐蔽而且经常被人忽视。很多 Java 开发在insert时没有做幂等校验而是直接插入然后捕获 DuplicateKeyException 做后续处理。这种写法在并发场景下会引发死锁。复现步骤事务 1 尝试插入一条user_code U1004的记录。事务 2 尝试插入同一条user_code U1004的记录。第一个插入请求会获取唯一索引的排他锁并插入成功或处于未提交状态。第二个插入请求检测到唯一键冲突会尝试获取该唯一索引上已存在记录的锁以便判断是“真的冲突”还是“唯一键被并发事务占用了”。此时形成等待。在 InnoDB 中唯一键冲突时后插入的事务需要等待第一个事务提交或回滚才能确定冲突是否真正存在。如果存在其他行锁交叉等待就可能进入死锁。解决办法使用INSERT ... ON DUPLICATE KEY UPDATE代替先查再插。在 Java 业务中如果无法使用数据库原生 upsert应该用分布式锁或 Redis 锁对其他并发插入做单机/全局串行化。捕获DuplicateKeyException后不要立即继续写库而应该做事务回滚并重新读取状态。3.3 场景三Gap Lock 与插入意向锁冲突这是 MySQLREPEATABLE READ隔离级别下非常典型的问题。Gap Lock 是区间锁锁的是“记录之间的区间”。例如WHERE balance 50不走唯一索引的情况下InnoDB 会对balance索引范围内的记录和间隙都加锁防止其他事务在这个范围内插入新数据。当两个事务同时对一个范围加 Gap Lock并且各自想要插入数据到对方锁定的间隙中时就发生死锁。举例表中有balance值 100、200、300 三条记录一个范围(100, 300)。事务 1 执行BEGIN; SELECT * FROM t_user_account WHERE balance BETWEEN 100 AND 300 FOR UPDATE;此时事务 1 对 balance 范围内已有的记录和间隙加锁。事务 2 执行同样的查询BEGIN; SELECT * FROM t_user_account WHERE balance BETWEEN 100 AND 300 FOR UPDATE;在REPEATABLE READ下这种查询用的是当前读也会对同样的范围加 Gap Lock。此时两个事务的 Gap Lock 其实是兼容的这个细节取决于版本和索引情况但真正冲突发生在其中一方尝试插入新数据时。如果事务 1 插入一条 balance150 的记录事务 2 也插入一条 balance150 的记录两者分别等待对方释放 Gap Lock就会死锁。解决办法如果业务对范围查询不是严格要求严谨的“可重复读快照”可以降低隔离级别到READ COMMITTED。这个级别下 InnoDB 会禁用 Gap Lock仅保留外键检查和唯一键检查时的必要锁。为范围查询条件建立合适的联合索引尽量使用精确匹配减少区间扫描范围。如果确实需要 Gap Lock 保证安全性那就需要通过代码将并发度串行化或者用LOCK IN SHARE MODE的语义重新设计业务流程。3.4 场景四单个 SQL 语句内部锁顺序交错有些死锁不是发生在两个事务之间而是发生在一条 SQL 内部。例如一条UPDATE语句使用多表关联更新或者使用子查询更新InnoDB 在执行过程中可能按某个顺序获取锁而另一条 SQL 以相反顺序获取同一批行的锁。这种案例相对少见但排查难度最大。因为死锁日志里的 SQL 只有一条光看 SQL 本身看不出两个事务各自的加锁轨迹。排查建议查看死锁日志中的TRANSACTION区块找到每个事务持有的lock struct和等待的RECORD LOCKS。配合EXPLAIN查看 SQL 的执行计划判断哪些索引会被用到。注意通过EXPLAIN看到的是当前优化器选择的执行计划实际执行时锁定的范围可能更大特别是有范围条件时。4. 死锁的完整排查流程当线上真的出现死锁告警不能慌也别急着重启服务。下面是一套标准排查流程可以做成 SOP 交给团队。4.1 第一步保存死锁现场发生死锁时第一时间执行SHOW ENGINE INNODB STATUS\G不同的 MySQL 版本和框架版本在锁信息和字段名上存在少量差异但这不影响整体排查思路。把输出保存到本地文件重点找LATEST DETECTED DEADLOCK。如果死锁频繁发生一次一次去线上执行SHOW ENGINE INNODB STATUS效率太低。建议开启 MySQL 错误日志并定期巡检错误日志中的死锁信息。在 MySQL 配置文件中可以开启相关日志[mysqld] # 开启通用日志或者错误日志 log_error /var/log/mysql/error.log # 慢查询日志 slow_query_log ON slow_query_log_file /var/log/mysql/slow-query.log long_query_time 2注意通用日志和慢查询日志在生产环境要谨慎开启并配合日志清理策略避免磁盘被写满。4.2 第二步查看当前运行中的事务SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_rows_locked, trx_rows_modified FROM information_schema.INNODB_TRX;这个查询能帮你看到当前数据库里所有活动事务。重点观察trx_started事务开始时间如果很长说明事务可能没有及时提交。trx_query事务当前正在执行的 SQL如果为 NULL说明事务空闲。trx_rows_locked锁定的行数过多就要警惕。4.3 第三步查看锁等待关系SELECT waiting_trx_id, waiting_lock_id, waiting_pid, blocking_trx_id, blocking_lock_id, blocking_pid FROM information_schema.INNODB_LOCK_WAITS;在 MySQL 8.0 中这张表可能被新的性能字典表替代如果查询报错可以使用SELECT w.BLOCKING_PID, w.BLOCKING_TRX_ID, w.BLOCKING_LOCK_ID, w.WAITING_PID, w.WAITING_TRX_ID, w.WAITING_LOCK_ID FROM performance_schema.data_lock_waits w;根据等待关系可以找到锁的“根事务”。如果是类似“事务 A 等待事务 B事务 B 等待事务 A”的闭环就是死锁如果是链式等待说明事务 A 阻塞了后面一串事务需要优先排查事务 A 为什么长时间不提交。4.4 第四步定位具体阻塞的 SQL找到阻塞事务的trx_mysql_thread_id就是数据库连接线程 ID通过SHOW PROCESSLIST或SELECT * FROM performance_schema.threads可以找到对应的连接信息包括连接的客户端 IP、连接时间、当前执行状态。SHOW FULL PROCESSLIST;如果阻塞事务的当前 SQL 为 NULL说明事务处于空闲状态但事务并未提交仍然持有锁。这在很多 Java 项目中非常常见业务代码里一个Transactional方法内调用了外部 HTTP 接口网络延迟导致事务长时间不提交行锁一直被持有后面所有请求全部堆积。4.5 第五步结合日志和代码分析数据库层面的信息只是“现象”最终的根因往往埋在业务代码里。排查时要把以下信息放在一起死锁日志中的 SQL 和锁等待记录。应用系统的日志时间线看看哪些请求在同一时间点并发执行。业务代码中对应方法的事务边界、方法调用链。重点检查代码中的三个环节Transactional注解的范围是否过大。事务方法里是否有外部 RPC 调用、网络请求、文件操作。多个业务方法是否以不同顺序更新了同一批数据。5. Java 项目的死锁根治方案排查清楚死锁原因后要从代码层面彻底解决。这个环节是 Java 面试中真正的加分项也是线上问题处理能力的最好体现。5.1 统一锁顺序最直接、最有效的方案是“全局约定加锁顺序”。例如所有涉及多行更新的操作都先按user_code排序/** * 批量更新用户余额统一按 userCode 排序避免死锁 */ public void batchUpdateBalances(ListUserAccount accounts) { // 核心所有线程都按同样的顺序更新 accounts.sort(Comparator.comparing(UserAccount::getUserCode)); for (UserAccount account : accounts) { userAccountMapper.updateBalance(account.getUserCode(), account.getBalance()); } }这段代码的精髓在于Comparator.comparing(UserAccount::getUserCode)。多个线程并发执行批量更新时无论传入的用户列表顺序如何最终都会先更新user_code较小的数据再更新较大的数据。这样就不会形成循环等待。5.2 缩小事务边界在 Spring Boot 项目中Transactional使用不当是锁等待和死锁的重要诱因。很多开发同学在方法上直接加注解导致整个方法内部的所有操作都在一个事务里包括耗时最长的网络请求。看下面的坏例子Transactional public void transferWithBadDesign(String from, String to, BigDecimal amount) { UserAccount fromAccount userAccountMapper.selectByCodeForUpdate(from); UserAccount toAccount userAccountMapper.selectByCodeForUpdate(to); // 模拟外部接口调用比如风控校验、短信通知 riskControlService.check(fromAccount, toAccount); boolean success riskControlService.sendNotify(); if (!success) { throw new BusinessException(通知失败); } userAccountMapper.decreaseBalance(from, amount); userAccountMapper.increaseBalance(to, amount); }这段代码有两个明显问题加锁顺序依赖参数传入顺序。如果两个请求的方向相反A 转 B、B 转 A很容易死锁。外部接口调用放在事务内导致事务时间被无限拉长锁被持有几十毫秒甚至几秒。改进后的写法Transactional public void transfer(String from, String to, BigDecimal amount) { // 先排序再按顺序加锁 String[] ordered sortKeys(from, to); UserAccount fromAccount userAccountMapper.selectByCodeForUpdate(ordered[0]); UserAccount toAccount userAccountMapper.selectByCodeForUpdate(ordered[1]); // 只做数据库操作 userAccountMapper.decreaseBalance(from, amount); userAccountMapper.increaseBalance(to, amount); }外部调用、风控校验、日志记录、消息发送全部放到事务外的其他方法中最好通过 Spring 的事务传播机制拆分。5.3 利用数据库层兜底锁等待超时即使代码层面做了大量优化仍然无法保证 100% 避免死锁。此时可以在数据库层配置锁等待超时让锁等待不会无限持续。[mysqld] # 锁等待超时时间默认是 50 秒 innodb_lock_wait_timeout 5 # 死锁检测默认开启 innodb_deadlock_detect ON生产环境中将innodb_lock_wait_timeout设置得过小会导致正常业务频繁失败设置得过大又会导致锁等待的事务长时间堆积。一般建议先设置为 510 秒根据业务 SQL 的执行耗时动态调整。5.4 Java 侧的重试机制由于 MySQL 检测到死锁时会回滚其中一个事务业务系统必须对死锁异常做重试处理。Spring Boot 场景下可以通过 AOP 统一拦截死锁异常并重试。Component Aspect public class RetryDeadLockAspect { private static final Logger log LoggerFactory.getLogger(RetryDeadLockAspect.class); private static final int MAX_RETRY_TIMES 3; /** * 拦截所有标注了 RetryOnDeadLock 的方法遇到死锁后重试 */ Around(annotation(com.example.demo.annotation.RetryOnDeadLock)) public Object retry(ProceedingJoinPoint joinPoint) throws Throwable { int retryCount 0; while (retryCount MAX_RETRY_TIMES) { try { return joinPoint.proceed(); } catch (DeadlockLoserDataAccessException e) { retryCount; log.warn(方法执行遇到死锁准备第 {} 次重试, retryCount, e); if (retryCount MAX_RETRY_TIMES) { throw e; } Thread.sleep(50); } } throw new RuntimeException(重试失败); } }在 Spring Boot 项目中常见的死锁异常有org.springframework.dao.DeadlockLoserDataAccessExceptionorg.springframework.dao.CannotAcquireLockExceptionorg.mybatis.spring.MyBatisSystemException底层 cause 是死锁错误码 1213重试的前提条件是死锁导致被回滚的事务是当前事务。当前业务操作是幂等的或者重试不会产生重复数据。重试次数不能太多必须设置最大次数上限。重试之间要有短暂间隔避免雪崩式地争抢锁。5.5 使用乐观锁避免行锁争用某些业务场景完全不需要使用悲观锁。例如库存扣减、账户余额更新可以直接用乐观锁实现。乐观锁不会产生死锁因为它没有“等待其他事务释放锁”的过程。Transactional public boolean deductStock(Long productId, BigDecimal deductCount) { ProductStock stock productStockMapper.selectById(productId); // 更新时带上版本号 int rows productStockMapper.deductWithVersion( productId, deductCount, stock.getVersion() ); // rows 1 表示更新成功rows 0 表示版本冲突需要重试 return rows 0; }对应的 SQL 是UPDATE product_stock SET stock stock - #{deductCount}, version version 1 WHERE product_id #{productId} AND version #{version} AND stock #{deductCount};乐观锁的适用条件是并发冲突频率不高或者冲突后重试代价较小。如果并发冲突非常激烈乐观锁会导致大量无用的重试倒不如优化为数据库原子操作UPDATE product_stock SET stock stock - #{deductCount} WHERE product_id #{productId} AND stock #{deductCount};这种原子更新在单行上执行不涉及多行加锁也不存在死锁问题。这也是很多高并发秒杀系统常用的扣减方案。它不需要显式地开启事务、先查后更新而是依赖单条 SQL 的原子性。6. 高频问题排查汇总实际项目中死锁和慢 SQL、锁等待经常一起出现。下面把最常见的现象、原因和处理思路整理成一张表方便对照排查。问题现象常见原因解决思路应用日志出现 Deadlock found错误码 1213两个事务加锁顺序不一致统一加锁顺序增加 Java 重试机制大量请求堆积接口 RT 飙升某个事务长时间持有锁未提交检查Transactional范围内是否有外部调用应用启动后连接池迅速耗尽数据库锁等待时间过长连接被占满调大锁等待前先找阻塞事务kill 阻塞连接SHOW ENGINE INNODB STATUS看不到死锁死锁是历史发生当前已回滚开启日志并提前配置监控告警SELECT ... FOR UPDATE执行慢查询未走索引行锁升级表锁用 EXPLAIN 分析执行计划补索引唯一键冲突后程序卡住唯一键冲突引发锁等待使用 upsert避免先查再插Gap Lock 导致的死锁隔离级别是 REPEATABLE READ范围查询降级到 READ COMMITTED 或优化索引死锁重试后仍然失败重试没有等待时间或同一事务内重复锁冲突增加退避时间分析事务边界特别注意如果线上出现严重锁等待需要 kill 某个连接一定要先确认该连接对应的事务分支避免误杀核心业务。kill 语句示例-- 根据 information_schema.INNODB_TRX 查询到的 trx_mysql_thread_id KILL 12345;KILL 操作需要数据库账号具备PROCESS和SUPER权限并且要在业务低峰期操作尽可能先通过应用系统优雅关闭连接而不是直接在数据库层强制 kill。7. 生产环境最佳实践死锁问题不是一次修复就永远消失的。真正稳妥的做法是把“预防-检测-处理-复盘”形成闭环。7.1 代码规范层面所有更新操作必须走索引避免行锁升级为表锁。多个表的更新顺序在代码评审中要明确约定。Transactional事务方法中禁止出现外部 HTTP 调用、短信发送、消息队列发送等耗时操作。批量更新前先排序排序规则统一用业务主键或唯一键。尽量使用数据库原子更新减少“先查后更新”的悲观锁依赖。7.2 监控与告警层面数据库死锁和锁等待必须有监控指标推荐关注InnoDB 死锁次数。当前活动事务数。锁等待次数和平均等待时长。连接池使用率。事务平均执行时长和最大执行时长。监控平台可以选择 Prometheus Grafana也可以通过 MySQL 自带的状态变量观察SHOW GLOBAL STATUS LIKE Innodb_row_lock_waits; SHOW GLOBAL STATUS LIKE Innodb_row_lock_time_avg; SHOW GLOBAL STATUS LIKE Innodb_deadlocks;如果Innodb_deadlocks持续增长说明应用层有系统性的死锁问题需要尽快做代码优化。7.3 应急预案层面线上死锁告警后处理步骤应该固化到团队的应急预案中示例流程如下先保存SHOW ENGINE INNODB STATUS输出。根据INNODB_TRX和INNODB_LOCK_WAITS找出锁等待关系。如果阻塞事务属于某一个慢接口优先考虑熔断或降级该接口。确认高风险连接后由 DBA 或高级开发执行 KILL。分析死锁日志确定根因。修复代码发布并在监控平台上验证死锁指标下降。7.4 数据库配置层面不要盲目修改数据库参数。死锁问题的核心在业务代码配置只是兜底。推荐的几项配置如下[mysqld] # 锁等待超时单位秒 innodb_lock_wait_timeout 5 # 死锁检测默认是开启的不要随意关闭 innodb_deadlock_detect ON # 事务隔离级别如果业务允许可以设置为 READ COMMITTED transaction_isolation READ-COMMITTED如果业务对事务隔离级别要求很高必须使用REPEATABLE READ那就更需要在代码层面严格控制锁的粒度和顺序。8. 总结与 Java 面试学习建议回到开头的面试题线上 MySQL 死锁了怎么办如果只是回答“重启服务”在面试官眼里基本等于“我没有排查过这个问题”。合理的面试回答应该分层展开先解释死锁的本质两个或多个事务互相持有对方需要的锁形成循环等待。InnoDB 死锁检测机制会回滚其中一个事务。再说排查命令使用SHOW ENGINE INNODB STATUS查看死锁日志用INNODB_TRX和INNODB_LOCK_WAITS定位事务和锁等待关系。然后说解决方案统一加锁顺序、缩小事务边界、合理设计索引、避免外部调用在事务内、数据库层设置锁等待超时、Java 侧增加重试机制。最后补充工程经验监控死锁次数建立应急预案代码评审时检查Transactional使用。如果这篇文章对你有帮助可以收藏备用。也欢迎在评论区聊聊你遇到过的 MySQL 死锁场景——是两条 UPDATE 互相等待还是插入唯一键冲突或者 Gap Lock 的“隐蔽死锁”排查经验往往比面试八股文更有价值。
返回列表