资讯详情

资讯详情

MySQL死锁问题分析与解决方案

1. 死锁现场当数据库突然拒绝服务时那天凌晨2点15分我的手机突然开始疯狂报警。负责的电商平台订单系统出现大面积超时前端页面不断弹出系统繁忙提示。登录服务器后发现MySQL实例的CPU使用率异常飙升到90%而正常情况下这个时间段的负载应该不超过30%。快速检查错误日志大量这样的记录跳出来[ERROR] [MY-010526] [InnoDB] Transaction (PID 38721) is waiting for lock; try restarting transaction更严重的是监控显示活跃线程数已经突破500而且还在持续增长。这明显是典型的数据库死锁症状——多个事务相互持有对方需要的资源导致所有相关事务都无法继续执行。注意当出现大量try restarting transaction错误时80%的情况下都是死锁问题。此时不要贸然重启数据库先收集证据。我立即在MySQL客户端执行了SHOW PROCESSLIST发现大量会话卡在updating状态且持续时间都已超过30秒。这些会话主要涉及三张核心表orders订单主表、order_items订单明细和inventory库存表。2. SHOW ENGINE INNODB STATUS的深度解读2.1 获取原始死锁信息在MySQL终端执行SHOW ENGINE INNODB STATUS\G这个命令会输出InnoDB存储引擎的完整状态信息其中包含一个专门的LATEST DETECTED DEADLOCK章节记录最近发生的死锁详情。在我的案例中输出内容大致如下------------------------ LATEST DETECTED DEADLOCK ------------------------ 2023-08-20 02:17:23 0x7f8e5c3e6700 *** (1) TRANSACTION: TRANSACTION 422183, ACTIVE 12 sec starting index read mysql tables in use 3, locked 3 LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s) MySQL thread id 38721, OS thread handle 140123456789, query id 987654 localhost root updating UPDATE inventory SET stock stock - 1 WHERE sku_id SKU001 *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 45 page no 3 n bits 72 index PRIMARY of table ecommerce.order_items trx id 422183 lock_mode X locks rec but not gap ... *** (2) TRANSACTION: TRANSACTION 422184, ACTIVE 10 sec starting index read mysql tables in use 3, locked 3 5 lock struct(s), heap size 1136, 3 row lock(s) MySQL thread id 38722, OS thread handle 140123456790, query id 987655 localhost root updating UPDATE order_items SET status paid WHERE order_id ORD10086 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 45 page no 4 n bits 80 index PRIMARY of table ecommerce.inventory trx id 422184 lock_mode X locks rec but not gap ... *** WE ROLL BACK TRANSACTION (2)2.2 死锁日志的关键字段解析事务信息块TRANSACTION [id]事务的唯一标识符ACTIVE [time]事务已运行时间mysql tables in use涉及的表数量locked当前锁定的表数量锁等待信息LOCK WAIT表示该事务正在等待锁lock struct(s)锁结构的数量row lock(s)持有的行锁数量持有的锁详情RECORD LOCKS记录锁最常引发死锁的类型space id/page no锁定数据所在的表空间和页编号index PRIMARY锁定发生在主键索引上lock_mode X排他锁写锁2.3 可视化死锁关系将上述信息整理成表格更易理解事务ID正在执行的SQL已持有的锁等待的锁422183更新inventory表order_items表主键行锁inventory表某行锁422184更新order_items表inventory表主键行锁order_items表某行锁这就是典型的交叉等待死锁场景——事务A持有资源1等待资源2事务B持有资源2等待资源1。3. 死锁根因分析业务代码中的隐藏陷阱3.1 事务执行顺序问题检查应用程序代码发现存在这样的逻辑// 订单支付成功回调方法 Transactional public void onPaymentSuccess(String orderId) { // 步骤1更新订单状态 orderDao.updateStatus(orderId, paid); // 步骤2扣减库存 ListOrderItem items orderItemDao.listByOrder(orderId); for (OrderItem item : items) { inventoryDao.reduceStock(item.getSkuId(), item.getQuantity()); } // 步骤3记录支付日志 paymentLogDao.insert(...); }同时库存预警服务有另一个定时任务Transactional public void checkLowStockItems() { // 步骤A查询低库存商品 ListString lowStockSkus inventoryDao.listLowStock(); // 步骤B获取这些商品的关联订单 for (String sku : lowStockSkus) { ListOrderItem items orderItemDao.listBySku(sku); // 发送预警通知... } }3.2 锁的获取顺序不一致当两个事务并发执行时事务1支付回调先锁order_items表步骤1然后尝试锁inventory表步骤2事务2库存检查先锁inventory表步骤A然后尝试锁order_items表步骤B这就形成了经典的锁顺序不一致导致的死锁。经验法则在涉及多表更新的业务中确保所有事务都以相同的顺序访问表。比如约定总是先操作order表再操作order_items最后操作inventory。4. 解决方案与优化实践4.1 短期应急措施发现死锁后的立即处理-- 查看当前运行的所有事务 SELECT * FROM information_schema.INNODB_TRX; -- 终止造成死锁的会话 KILL 38721; KILL 38722;但要注意这只是临时解决方案必须配合下面的长期方案才能真正解决问题。4.2 事务重构统一锁获取顺序修改业务代码确保所有相关事务都按照相同顺序访问表// 统一约定先inventory再order_items最后orders Transactional public void onPaymentSuccess(String orderId) { // 新步骤1先扣减库存 ListOrderItem items orderItemDao.listByOrder(orderId); for (OrderItem item : items) { inventoryDao.reduceStock(item.getSkuId(), item.getQuantity()); } // 新步骤2更新订单状态 orderDao.updateStatus(orderId, paid); // 步骤3保持不变... }4.3 锁粒度优化使用SELECT ... FOR UPDATE明确锁定范围-- 原代码 UPDATE inventory SET stock stock - 1 WHERE sku_id SKU001; -- 优化后 BEGIN; SELECT * FROM inventory WHERE sku_id SKU001 FOR UPDATE; -- 业务逻辑判断... UPDATE inventory SET stock stock - 1 WHERE sku_id SKU001; COMMIT;添加合适的索引 检查发现order_items表的sku_id字段没有索引导致查询时锁定了整个表-- 添加索引前全表扫描 EXPLAIN SELECT * FROM order_items WHERE sku_id SKU001; -- type: ALL, key: NULL -- 添加索引 ALTER TABLE order_items ADD INDEX idx_sku_id (sku_id); -- 添加索引后索引扫描 EXPLAIN SELECT * FROM order_items WHERE sku_id SKU001; -- type: ref, key: idx_sku_id4.4 事务隔离级别调整检查当前隔离级别SELECT transaction_isolation; -- 默认通常是REPEATABLE-READ对于高并发场景可以考虑降级为READ-COMMITTEDSET GLOBAL transaction_isolation READ-COMMITTED;但要注意这可能导致幻读问题需要评估业务是否能够接受。4.5 死锁监控自动化配置定期收集死锁信息的脚本#!/bin/bash # 每5分钟记录一次死锁信息 while true; do mysql -e SHOW ENGINE INNODB STATUS\G | grep -A 50 LATEST DETECTED DEADLOCK /var/log/mysql_deadlocks.log sleep 300 done并在my.cnf中添加配置确保记录所有死锁[mysqld] innodb_print_all_deadlocks ON5. 深入理解InnoDB锁机制5.1 InnoDB锁类型详解共享锁(S锁)SELECT * FROM table WHERE id 1 LOCK IN SHARE MODE;允许其他事务同时获取S锁阻止任何事务获取X锁排他锁(X锁)SELECT * FROM table WHERE id 1 FOR UPDATE;阻止其他事务获取任何锁意向锁(Intention Locks)表级锁表示事务准备在表中的行上获取什么类型的锁IS意向共享锁IX意向排他锁5.2 记录锁、间隙锁和临键锁记录锁(Record Locks)锁定索引中的具体记录最常见于等值查询WHERE id 123间隙锁(Gap Locks)锁定索引记录之间的间隙防止其他事务插入到间隙中在REPEATABLE-READ隔离级别下自动启用临键锁(Next-Key Locks)记录锁间隙锁的组合锁定索引记录及其前面的间隙5.3 死锁检测与回滚机制InnoDB使用等待图(wait-for graph)算法检测死锁定期检查事务等待关系如果发现循环等待A等BB等CC等A选择回滚代价最小的事务通常是最新的事务返回错误ERROR 1213 (40001): Deadlock found可以通过参数调整死锁检测频率innodb_deadlock_detect ON # 默认开启 innodb_lock_wait_timeout 50 # 锁等待超时(秒)6. 高级排查技巧与工具6.1 performance_schema监控启用性能模式监控锁等待-- 启用锁监控 UPDATE performance_schema.setup_instruments SET ENABLED YES WHERE NAME LIKE %wait/lock%; -- 查看当前锁等待 SELECT * FROM performance_schema.events_waits_current WHERE EVENT_NAME LIKE %lock%;6.2 sys schema视图MySQL提供的sys schema包含许多有用的视图-- 查看当前锁等待关系 SELECT * FROM sys.innodb_lock_waits; -- 查看哪些事务持有锁 SELECT * FROM sys.schema_table_lock_waits;6.3 pt-deadlock-logger工具Percona Toolkit中的pt-deadlock-logger可以自动记录死锁pt-deadlock-logger hlocalhost,uroot,ppassword --iterations10输出示例2023-08-20T02:17:23 server:localhost victim_trx_id:422184 victim_thread:38722 victim_query:UPDATE order_items SET status paid WHERE order_id ORD10086 ...6.4 可视化分析工具将死锁日志转换为可视化图表使用pt-visual-deadlockPercona Toolkit组件或使用在线工具https://www.percona.com/live/mysql-deadlock-analyzer7. 预防死锁的最佳实践事务设计原则保持事务短小精悍避免在事务中包含用户交互按照固定顺序访问表和行索引优化确保查询都使用索引避免全表扫描会导致锁全表合理设计组合索引锁优化技巧尽量使用行锁而非表锁在事务开始时获取所有需要的锁考虑使用乐观锁替代悲观锁应用层防护实现重试机制捕获死锁异常后自动重试使用分布式锁控制并发限制长事务的执行时间监控与告警监控死锁率SHOW STATUS LIKE innodb_row_lock%设置死锁次数告警阈值定期分析死锁日志模式在实际项目中我们通过以上方法将死锁发生率降低了90%以上。关键是要理解业务场景中的数据访问模式设计合理的事务边界和锁策略。每次遇到死锁都是一次优化系统架构的机会而不是简单的错误修复。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →