公司动态
数据库死锁和多线程死锁的区别
数据库死锁与多线程死锁本质上都符合死锁的四个必要条件互斥、持有并等待、不可剥夺、循环等待但二者发生的层级、资源对象、检测机制、处理策略差异显著核心区别如下一、核心定义多线程死锁发生在应用进程内部两个或多个线程互相持有对方所需的锁资源同时永久等待对方释放锁导致线程全部阻塞无法执行属于应用层 / 操作系统层面的并发问题。数据库死锁发生在数据库引擎内部两个或多个并发事务互相持有对方需要的数据库锁行锁、表锁等并同时请求对方持有的锁形成循环等待导致事务无法推进属于数据库引擎层面的并发问题。二、核心区别对比1. 发生层级与主体不同多线程死锁主体是同一进程内的线程锁是进程内存中的锁对象如 Java 的synchronized、ReentrantLock依赖 JVM / 操作系统调度。数据库死锁主体是数据库事务通常来自不同的数据库连接可跨进程、跨应用服务锁由数据库引擎统一维护依赖数据库自身的并发控制机制。2. 竞争的资源类型不同多线程死锁竞争内存中的共享资源如对象锁、临界区代码、共享变量、连接池对象等资源是临时内存态的。数据库死锁竞争持久化数据的访问权限如数据行、数据表、索引页通过锁保障事务 ACID 特性资源对应磁盘上的持久化数据。3. 加锁方式与透明度不同多线程死锁加锁是显式、代码可控的开发者手动控制加解锁逻辑加锁顺序完全由业务代码决定。数据库死锁加锁多为隐式触发执行UPDATE/DELETE/SELECT ... FOR UPDATE时引擎会自动根据索引、隔离级别加行锁或表锁开发者难以直观感知锁的范围和顺序易因索引失效、SQL 执行顺序差异引发死锁。4. 死锁检测机制不同多线程死锁操作系统和 JVM不会主动检测与干预死锁发生后无自动恢复能力只能通过jstack、Arthas 等工具事后排查。数据库死锁主流数据库如 MySQL InnoDB内置主动死锁检测事务等待锁时引擎通过锁等待图检查是否存在循环依赖一旦检测到死锁立即触发处理无需人工干预。5. 处理与恢复能力不同多线程死锁一旦发生基本不可逆无自动恢复机制只能终止线程或重启进程以事前预防为主。少数场景可通过tryLock(timeout)超时放弃锁打破死锁仍属于事前预防手段。数据库死锁检测到死锁后数据库会自动回滚代价最小的事务如修改行数少、Undo 日志量小的事务释放锁让其余事务继续执行应用侧只需捕获异常并重试事务具备自愈能力。6. 锁粒度与持有特性不同多线程死锁锁粒度通常为代码块 / 对象级别设计良好的临界区锁持有时间极短毫秒级锁类型相对简单以独占锁、读写锁为主。数据库死锁锁粒度更丰富分为行级、页级、表级锁还有意向锁、共享锁S、排他锁X等细分类型锁持有时间横跨整个事务长事务可能持锁数秒甚至数分钟死锁概率随事务时长显著上升。7. 影响范围不同多线程死锁仅影响当前进程内的相关线程最坏情况导致单个应用进程卡死不波及其他服务。数据库死锁影响对应连接上的事务可能波及多个应用服务实例但数据库本身不会因死锁崩溃仅回滚单个事务整体可用性更高。8. 典型预防手段不同表格多线程死锁预防数据库死锁预防固定全局加锁顺序固定 SQL 的行访问顺序避免锁嵌套、减少锁持有时间缩短事务长度杜绝长事务使用tryLock超时机制合理设置锁等待超时参数用无锁并发CAS替代锁优化索引避免全表扫描升级表锁拆细分粒度降低冲突降低隔离级别如 RR→RC缩小锁范围三、共同点都满足死锁四大必要条件互斥性、请求与保持、不可剥夺、循环等待打破任意一个即可预防死锁。根本原因都是并发资源竞争 加锁顺序不一致。最优解均为 “预防优先”通过规范加锁顺序、缩短锁持有时间从根源避免而非依赖事后处理。一、Java 多线程死锁复现 排查1. 复现代码经典顺序反转场景核心原理两个线程以相反的顺序获取两个锁对象各自持有第一个锁后再去申请对方持有的第二个锁形成循环等待。public class ThreadDeadLockDemo { // 两个锁对象 private static final Object LOCK_1 new Object(); private static final Object LOCK_2 new Object(); public static void main(String[] args) { // 线程1先拿LOCK_1再拿LOCK_2 new Thread(() - { synchronized (LOCK_1) { System.out.println(Thread.currentThread().getName() 已持有锁1等待获取锁2); try { // 睡眠100ms确保线程2能先拿到锁2保证死锁触发 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LOCK_2) { System.out.println(Thread.currentThread().getName() 成功获取锁2); } } }, 线程-1).start(); // 线程2先拿LOCK_2再拿LOCK_1 new Thread(() - { synchronized (LOCK_2) { System.out.println(Thread.currentThread().getName() 已持有锁2等待获取锁1); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (LOCK_1) { System.out.println(Thread.currentThread().getName() 成功获取锁1); } } }, 线程-2).start(); } }2. 死锁现象程序运行后永远不会退出两个线程永久阻塞控制台只会打印两行持有锁的日志不会打印 “成功获取锁” 的日志。3. 排查命令与步骤1jstack 原生命令排查JDK 自带先找到 Java 进程 IDjps -l导出线程堆栈分析死锁jstack 进程ID关键输出解读 堆栈末尾会明确输出Found one Java-level deadlock:并标注每个阻塞线程的名称、状态BLOCKED该线程正在等待哪个锁、已经持有哪个锁锁对应的对象地址、所属线程2Arthas 快速定位生产环境常用# 进入Arthas后直接执行 thread -b该命令会直接找出阻塞其他线程的死锁线程并输出其栈信息比 jstack 更高效。二、MySQL 死锁复现 排查基于SELECT ... FOR UPDATE行锁复现与前文知识点衔接。核心原理两个事务以相反顺序锁定多行数据形成循环等待。1. 环境准备与复现步骤前置准备-- 建表必须InnoDB引擎主键索引保证行锁 CREATE TABLE user ( id int NOT NULL PRIMARY KEY, name varchar(20) DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 插入测试数据 INSERT INTO user VALUES (1, 张三), (2, 李四);复现操作开启 2 个 MySQL 会话窗口表格时间顺序会话 A事务 1会话 B事务 2现象1BEGIN;SELECT * FROM user WHERE id1 FOR UPDATE;事务 1 锁定 id1 行2BEGIN;SELECT * FROM user WHERE id2 FOR UPDATE;事务 2 锁定 id2 行3SELECT * FROM user WHERE id2 FOR UPDATE;事务 1 申请 id2 的锁被事务 2 持有阻塞等待4SELECT * FROM user WHERE id1 FOR UPDATE;事务 2 申请 id1 的锁循环等待形成触发死锁2. 死锁现象其中一个会话立即报错ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction另一个会话正常执行锁被释放。InnoDB 会自动回滚代价更小的事务修改数据量少、Undo 日志量小的事务。3. 排查命令与日志解读1查看最近一次死锁详情SHOW ENGINE INNODB STATUS;重点关注结果中的LATEST DETECTED DEADLOCK段落核心信息包括两个事务的 ID、执行的 SQL 语句每个事务已持有的锁类型、锁范围每个事务正在等待的锁最终回滚了哪个事务WE ROLL BACK TRANSACTION2实时查看锁与事务状态MySQL 8.0 推荐死锁发生前的阻塞阶段可通过以下 SQL 定位锁等待关系-- 1. 查看当前所有持有/等待的锁 SELECT * FROM performance_schema.data_locks; -- 2. 查看锁等待关系谁在等谁的锁 SELECT * FROM performance_schema.data_lock_waits; -- 3. 查看所有运行中的事务状态、持锁时长 SELECT trx_id, trx_state, trx_wait_started, trx_rows_locked FROM information_schema.innodb_trx;3持久化记录所有死锁日志默认死锁日志只保留最近一条可开启全局配置将所有死锁记录到 MySQL 错误日志中方便事后排查SET GLOBAL innodb_print_all_deadlocks 1;如何避免 Java 多线程死锁死锁的核心是四大必要条件互斥、持有并等待、不可剥夺、循环等待同时成立避免死锁的核心就是打破其中任意一个条件。其中「互斥」是锁的固有属性保障线程安全的基础无法打破工程上主要从另外三个条件入手结合 Java 语言特性有以下成熟方案。一、打破「循环等待」统一全局加锁顺序原理所有线程按照完全一致的顺序申请多个锁从根源上避免 “你等我、我等你” 的循环依赖。这是最常用、成本最低的死锁预防方案。具体做法给所有锁资源定义唯一的排序规则如按对象哈希值、业务 ID 大小、锁名称字典序所有线程必须严格按照统一顺序加锁禁止反向嵌套基础示例public class FixOrderDeadLockAvoid { private static final Object LOCK_A new Object(); private static final Object LOCK_B new Object(); // 所有方法都遵循先加LOCK_A再加LOCK_B public void method1() { synchronized (LOCK_A) { // 前置业务逻辑 synchronized (LOCK_B) { // 临界区代码 } } } public void method2() { // 即使业务逻辑不同也必须遵守相同的加锁顺序 synchronized (LOCK_A) { synchronized (LOCK_B) { // 临界区代码 } } } }通用排序写法适配任意锁对象 当锁对象不固定时通过System.identityHashCode()计算唯一哈希值排序确保全局顺序一致public void safeLock(Object lock1, Object lock2) { // 按系统哈希值排序确定加锁先后 Object first System.identityHashCode(lock1) System.identityHashCode(lock2) ? lock1 : lock2; Object second first lock1 ? lock2 : lock1; synchronized (first) { synchronized (second) { // 业务逻辑 } } }二、打破「持有并等待」一次性申请所有资源原理线程必须一次性申请到所有需要的锁才开始执行业务只要有一个锁拿不到就不持有任何锁并重试避免 “拿着一个锁死等另一个”。synchronized不支持非阻塞抢锁该方案通常配合ReentrantLock实现import java.util.Arrays; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class BatchLockManager { // 按哈希排序后一次性申请所有锁全部成功返回true失败则全部释放 public boolean tryAcquireAll(Lock... locks) { // 先排序保证加锁顺序一致同时避免循环等待 Arrays.sort(locks, (a, b) - Integer.compare(System.identityHashCode(a), System.identityHashCode(b))); int acquiredCount 0; for (Lock lock : locks) { if (lock.tryLock()) { acquiredCount; } else { // 有一个锁失败反向释放所有已获取的锁 for (int i acquiredCount - 1; i 0; i--) { locks[i].unlock(); } return false; } } return true; } // 批量释放锁 public void releaseAll(Lock... locks) { for (Lock lock : locks) { lock.unlock(); } } }使用时只需循环重试直到拿到所有锁while (!batchLockManager.tryAcquireAll(lock1, lock2)) { // 短暂休眠避免活锁 Thread.sleep(10); } try { // 执行业务逻辑 } finally { batchLockManager.releaseAll(lock1, lock2); }三、打破「不可剥夺」锁超时主动放弃原理抢锁时设置超时时间若超时仍未获取到锁主动放弃并释放当前已持有的所有锁避免永久阻塞。这是synchronized不具备的能力必须使用ReentrantLock实现。代码示例import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TimeoutLockAvoid { private final Lock lock1 new ReentrantLock(); private final Lock lock2 new ReentrantLock(); public void businessMethod() throws InterruptedException { while (true) { // 尝试获取第一个锁超时则重试 if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 持有锁1的前提下尝试获取锁2 if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 两个锁都获取成功执行业务逻辑 System.out.println(执行业务逻辑); return; } finally { lock2.unlock(); } } } finally { // 无论锁2是否成功都必须释放锁1 lock1.unlock(); } } // 抢锁失败随机休眠后重试避免活锁 TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10)); } } }关键注意点所有解锁操作必须放在finally中防止异常导致锁泄漏配合随机延迟重试避免多个线程同时重试、同时失败的活锁问题四、从根源减少锁依赖降低锁粒度 无锁替代很多死锁本质是锁嵌套过多、锁粒度太粗导致的优先从设计层面减少锁的使用从根源规避死锁风险。避免锁嵌套缩短锁持有时间不要在锁代码块内部嵌套获取其他锁尽量拆分临界区将耗时操作RPC 调用、磁盘 IO、复杂计算移出锁范围只对共享变量的读写加锁用无锁并发替代重量级锁对于简单原子操作优先使用 JUC 原子类基于 CAS 无锁算法完全避免锁的使用计数 / 累加AtomicInteger、AtomicLong、LongAdder对象引用更新AtomicReference字段原子更新AtomicIntegerFieldUpdater优先使用内置线程安全工具避免自己手写锁逻辑直接使用 JDK 并发包中的线程安全容器替代 HashMapConcurrentHashMap替代 ArrayListCopyOnWriteArrayList任务协作CountDownLatch、Semaphore、CyclicBarrier锁分离优化读多写少场景使用ReentrantReadWriteLock读写锁读锁之间不互斥大幅降低锁冲突概率间接减少死锁可能。五、工程化兜底与规范禁止在锁内调用未知外部方法锁内调用第三方接口、回调方法或其他类的公共方法时对方内部可能也存在加锁逻辑会引入不可控的锁嵌套极易触发死锁。线上死锁检测与告警可通过 JMX 的ThreadMXBean编写定时检测逻辑发现死锁立即告警ThreadMXBean bean ManagementFactory.getThreadMXBean(); long[] deadlockedThreads bean.findDeadlockedThreads(); if (deadlockedThreads ! null deadlockedThreads.length 0) { // 触发告警输出线程栈信息 }全局编码规范约束团队内统一加锁规范代码评审重点检查锁嵌套、加锁顺序不一致的问题。方案选型总结表格方案适用场景优点缺点统一加锁顺序锁数量固定、业务场景明确实现简单无额外性能损耗需全局规范约束易被破坏锁超时机制高并发、允许短暂重试的场景灵活可自动恢复可能出现活锁有重试开销一次性申请资源锁数量少、临界区短的场景彻底避免持有并等待资源利用率低吞吐下降无锁 / 细粒度锁简单原子操作、读多写少场景性能最优从根源规避死锁不适用复杂多变量原子操作一、可直接复用的批量加锁工具类设计核心自动排序加锁基于System.identityHashCode对锁对象全局排序从根源打破循环等待条件失败自动回滚批量加锁过程中任意一个锁获取失败立即逆序释放所有已持有的锁杜绝锁泄漏超时自动放弃支持超时时间配置打破不可剥夺条件避免永久阻塞自动释放兜底实现AutoCloseable接口配合try-with-resources语法无需手动写 finally 解锁完整工具类代码import java.util.Arrays; import java.util.Comparator; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; /** * 批量锁工具类统一加锁顺序自动释放避免死锁 */ public class LockUtils { /** * 锁持有结果配合 try-with-resources 自动释放 */ public static class LockHolder implements AutoCloseable { private final Lock[] locks; private boolean acquired; private LockHolder(Lock[] locks, boolean acquired) { this.locks locks; this.acquired acquired; } /** * 是否成功获取所有锁 */ public boolean isAcquired() { return acquired; } Override public void close() { if (acquired) { // 逆序释放和加锁顺序相反 for (int i locks.length - 1; i 0; i--) { locks[i].unlock(); } acquired false; } } } /** * 尝试批量获取所有锁立即返回不阻塞 * param locks 待获取的锁列表 * return 锁持有结果使用 try-with-resources 自动释放 */ public static LockHolder tryAcquireAll(Lock... locks) { if (locks null || locks.length 0) { return new LockHolder(new Lock[0], true); } // 按系统哈希值排序保证全局加锁顺序一致 Lock[] sortedLocks Arrays.copyOf(locks, locks.length); Arrays.sort(sortedLocks, Comparator.comparingInt(LockUtils::identityHash)); int acquiredCount 0; for (Lock lock : sortedLocks) { if (lock.tryLock()) { acquiredCount; } else { // 获取失败逆序释放所有已持有的锁 for (int i acquiredCount - 1; i 0; i--) { sortedLocks[i].unlock(); } return new LockHolder(sortedLocks, false); } } return new LockHolder(sortedLocks, true); } /** * 带超时的批量锁获取 * param timeout 总超时时间 * param unit 时间单位 * param locks 待获取的锁列表 * return 锁持有结果 * throws InterruptedException 线程被中断时抛出 */ public static LockHolder tryAcquireAll(long timeout, TimeUnit unit, Lock... locks) throws InterruptedException { if (locks null || locks.length 0) { return new LockHolder(new Lock[0], true); } Lock[] sortedLocks Arrays.copyOf(locks, locks.length); Arrays.sort(sortedLocks, Comparator.comparingInt(LockUtils::identityHash)); long remainingNanos unit.toNanos(timeout); long deadline System.nanoTime() remainingNanos; int acquiredCount 0; for (Lock lock : sortedLocks) { if (!lock.tryLock(remainingNanos, TimeUnit.NANOSECONDS)) { // 超时未获取到释放已持有锁 for (int i acquiredCount - 1; i 0; i--) { sortedLocks[i].unlock(); } return new LockHolder(sortedLocks, false); } acquiredCount; remainingNanos deadline - System.nanoTime(); // 剩余时间不足直接失败释放 if (remainingNanos 0) { for (int i acquiredCount - 1; i 0; i--) { sortedLocks[i].unlock(); } return new LockHolder(sortedLocks, false); } } return new LockHolder(sortedLocks, true); } /** * 获取对象的系统身份哈希值避免重写hashCode导致排序不稳定 */ private static int identityHash(Object obj) { return System.identityHashCode(obj); } // 工具类禁止实例化 private LockUtils() {} }使用示例import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class LockUtilsDemo { private static final ReentrantLock orderLock new ReentrantLock(); private static final ReentrantLock stockLock new ReentrantLock(); private static final ReentrantLock userLock new ReentrantLock(); public void placeOrder() throws InterruptedException { // 方式1快速尝试失败立即重试 while (true) { try (LockUtils.LockHolder holder LockUtils.tryAcquireAll(orderLock, stockLock, userLock)) { if (holder.isAcquired()) { // 执行业务逻辑扣库存、生成订单、扣用户余额 System.out.println(订单业务执行完成); break; } } // 随机休眠避免活锁 TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10)); } // 方式2带超时超过时间直接失败 try (LockUtils.LockHolder holder LockUtils.tryAcquireAll(500, TimeUnit.MILLISECONDS, orderLock, stockLock)) { if (holder.isAcquired()) { System.out.println(执行业务逻辑); } else { throw new RuntimeException(系统繁忙请稍后重试); } } } }二、Java 开发常见死锁踩坑案例案例 1锁内调用外部方法 → 隐式锁嵌套场景在同步方法 / 锁块内调用第三方接口、回调方法或其他类的公共方法对方方法内部也有加锁逻辑形成不可控的锁嵌套。问题代码示例public class UserService { private final Object userLock new Object(); private OrderService orderService; public void updateUser() { synchronized (userLock) { // 修改用户信息 // 锁内调用外部服务orderService内部也有锁 orderService.updateOrderByUser(); } } } public class OrderService { private final Object orderLock new Object(); private UserService userService; public void updateOrderByUser() { synchronized (orderLock) { // 修改订单 } } public void cancelOrder() { synchronized (orderLock) { // 锁内调用用户服务 userService.updateUser(); } } }死锁原因线程 A 执行updateUser持有 userLock调用 orderService 等待 orderLock线程 B 执行cancelOrder持有 orderLock调用 userService 等待 userLock形成循环等待。修复方案严格遵循「锁内不调用外部方法」原则将外部调用移出锁块若必须调用统一全局加锁顺序或使用上述批量锁工具类案例 2读写锁锁升级死锁场景使用ReentrantReadWriteLock时持有读锁的状态下直接申请写锁造成锁升级死锁。问题代码示例ReentrantReadWriteLock rwLock new ReentrantReadWriteLock(); rwLock.readLock().lock(); try { // 读操作 if (needUpdate) { // 直接升级写锁 → 永久阻塞 rwLock.writeLock().lock(); try { // 写操作 } finally { rwLock.writeLock().unlock(); } } } finally { rwLock.readLock().unlock(); }死锁原因ReentrantReadWriteLock不支持锁升级读→写写锁必须等待所有读锁释放当前线程自己持有读锁又等待写锁形成自我阻塞。修复方案先释放读锁再获取写锁牺牲一致性业务可接受时使用直接使用写锁替代放弃读写分离使用支持锁升级的锁如StampedLock的乐观读升级案例 3不同方法加锁顺序不一致场景同一个类的多个方法对多个锁的加锁顺序不统一是最经典也最容易忽略的死锁场景。问题代码示例public void methodA() { synchronized (lockA) { synchronized (lockB) { // 业务逻辑 } } } public void methodB() { synchronized (lockB) { synchronized (lockA) { // 业务逻辑 } } }死锁原因两个线程分别执行 methodA 和 methodB各自持有第一个锁后等待第二个锁形成循环等待。修复方案全局统一加锁顺序所有方法必须按固定顺序加锁使用上述批量锁工具类自动排序加锁杜绝人工顺序错误案例 4线程池任务依赖死锁场景线程池中的任务提交新任务并同步等待结果当线程池线程被占满时所有任务都在等待子任务执行而子任务永远拿不到线程执行。问题代码示例ExecutorService executor Executors.newFixedThreadPool(2); // 主线程提交任务A FutureString futureA executor.submit(() - { // 任务A内部再提交任务B并等待结果 FutureString futureB executor.submit(() - 任务B结果); return futureB.get(); // 阻塞等待任务B }); futureA.get();当线程池只有 2 个线程同时提交 2 个这样的任务时两个线程都在等待子任务子任务永远得不到执行形成死锁。修复方案拆分线程池父子任务使用不同的线程池避免任务间同步等待改为异步回调调大线程池容量治标不治本高并发仍会触发案例 5同步集合迭代隐式加锁场景使用Collections.synchronizedList等同步包装类时迭代遍历会持有集合锁若迭代内部再持有其他锁容易形成死锁。问题代码示例ListString syncList Collections.synchronizedList(new ArrayList()); Object otherLock new Object(); // 线程1 synchronized (otherLock) { // 迭代同步集合会隐式持有集合的锁 for (String s : syncList) { // ... } } // 线程2 synchronized (syncList) { // 显式持有集合锁 synchronized (otherLock) { // 等待otherLock // ... } }死锁原因同步集合的迭代器内部会加集合对象锁和外部锁顺序相反时形成循环等待。修复方案优先使用ConcurrentHashMap、CopyOnWriteArrayList等并发容器避免隐式锁必须使用同步集合时明确所有加锁顺序避免嵌套小结实际项目中绝大多数死锁都源于「锁嵌套 顺序不一致」最优解优先级优先减少锁的使用用无锁原子类、并发容器替代必须加锁时尽量避免锁嵌套缩短锁持有时间必须多锁嵌套时强制统一加锁顺序或使用批量锁工具类兜底用超时机制避免永久阻塞面试中「线上死锁」的核心含义面试里提到的线上死锁不是单指某一种死锁概念而是泛指生产环境运行过程中真实发生的死锁故障主要分为「Java 应用层多线程死锁」和「数据库层事务死锁」两大类。面试官问这个问题本质不是考死锁的定义而是考察你有没有线上故障排查的实战经验能不能完整讲清「发现告警→应急止血→定位根因→修复预防」的全流程以及对死锁底层原理的理解。一、两类线上死锁的定义与生产表现1. Java 应用层多线程死锁本质Java 服务进程内多个线程互相持有对方需要的锁资源永久阻塞谁都无法继续执行属于应用层并发故障。线上典型现象服务接口大量超时、成功率骤降但 CPU 使用率不高线程都处于阻塞状态不消耗 CPU线程池被占满后续请求全部排队或直接拒绝服务呈现 “假死” 状态重启后暂时恢复高并发下会再次复现无自动恢复能力不干预会永久阻塞2. 数据库层MySQL InnoDB死锁本质数据库中多个并发事务互相持有对方需要的锁行锁、间隙锁等形成循环等待属于数据库引擎层的并发故障。线上典型现象接口偶发报错错误信息包含Deadlock found when trying to get lockMySQL 错误码 1213不是 100% 失败重试后大概率成功InnoDB 会自动回滚代价最小的事务释放锁资源错误率随并发量上升而升高服务本身不会整体假死仅冲突的事务执行失败二、面试标准答题框架体现线上实战经验面试官问 “你遇到过的线上死锁是怎么处理的”建议按「发现告警 → 应急止血 → 定位根因 → 根治预防」四步作答逻辑清晰且完全符合真实生产排障流程。1. 发现靠监控告警主动感知线上死锁不能等用户反馈要靠监控主动发现应用层监控监控线程池活跃线程数、阻塞线程数、接口超时率 / 成功率指标异常触发告警数据库监控监控死锁次数、锁等待时长、长事务数量开启innodb_print_all_deadlocks 1持久化所有死锁日志业务监控核心接口成功率、订单创建失败率等业务指标异常反向触发技术排查2. 应急先恢复业务再排查问题线上故障第一优先级是止血不是定位根因Java 线程死锁应急先将故障实例从集群摘流避免影响整体流量重启服务实例快速恢复业务同时保留故障节点的线程栈快照用于事后排查数据库死锁应急临时降级热点写接口降低并发冲突杀掉持有锁的长事务快速释放锁资源紧急修复失效索引避免全表锁扩大影响范围3. 定位留存现场精准排查Java 死锁排查步骤用jps -l定位 Java 进程 ID用jstack pid导出线程栈搜索Found one Java-level deadlock关键字直接定位死锁线程、已持有的锁、正在等待的锁生产环境常用 Arthas 执行thread -b一键找出阻塞其他线程的死锁线程无需重启服务MySQL 死锁排查步骤执行SHOW ENGINE INNODB STATUS;查看最近一次死锁详情重点分析两个事务的执行 SQL、持有的锁类型、等待的锁资源实时排查用performance_schema.data_locks和data_lock_waits查看当前锁持有与等待关系结合业务代码核对 SQL 执行顺序、条件字段是否命中索引、事务隔离级别4. 根治从代码和设计层面预防Java 端预防强制统一全局加锁顺序禁止反向嵌套加锁优先使用ReentrantLock配合tryLock超时机制打破永久等待减少锁嵌套锁内禁止调用外部方法、执行耗时 IO优先用原子类、并发容器替代手动加锁从根源减少锁依赖数据库端预防所有事务按固定顺序更新数据行如按主键 ID 从小到大保证更新条件命中索引避免无索引导致全表锁缩短事务长度禁止长事务锁内不掺杂非数据库业务逻辑业务允许时使用 RC 隔离级别减少间隙锁引发的死锁三、面试常见追问预判怎么区分线上是 Java 死锁还是数据库死锁看报错栈Java 死锁表现为线程处于BLOCKED状态无数据库错误码数据库死锁会抛出SQLException错误码 1213日志明确包含死锁关键字。线上 Java 死锁必须重启服务吗不是必须。可以用 Arthas 等工具 attach 到进程直接终止死锁线程但生产环境不推荐这种操作重启是最稳妥、最快的止血方案排查可以在留存的快照上离线分析。死锁一定会导致服务不可用吗不一定。Java 死锁如果只阻塞非核心线程可能只影响部分功能数据库死锁只会回滚冲突事务其他事务正常执行表现为偶发失败不会导致整体服务不可用。