[线上问题排查]1.数据库死锁全解析与解决方案
目录
1.什么是数据库死锁?
2.死锁产生的必要条件
3.死锁产生的原因
1. 多个事务之间的循环等待
2. 表级锁与行级锁混合(意向锁的存在)
3. 间隙锁(Gap Lock)与临键锁(Next-Key Lock)导致的死锁
4. 不同索引访问路径导致的锁冲突
总结与启示
4.如何排查数据库死锁
4.1. 开启死锁日志记录
4.2. 监控死锁信息
4.3. 分析死锁日志
4.4. 使用性能Schema监控(MySQL 5.6+)
4.5. 使用sys Schema快速诊断(MySQL 5.7+)
5.解决死锁的常用方法
1. 事务设计优化
核心思想:打破死锁产生的四个必要条件
一、 保持事务简短且高效
二、 严格遵守统一的访问顺序
三、 基于版本的乐观锁(Optimistic Locking)
四、 精细化锁的粒度:尽量使用行锁
五、 总结与实战 checklist
2. SQL优化
一、 减少锁定范围:索引是关键
1. 为 WHERE 子句和 UPDATE 条件建立索引
2. 使用主键或唯一索引进行更新
3. 理解并小心间隙锁(Gap Lock)
二、 避免热点更新:使用CAS(Compare-and-Swap)
传统方式 vs CAS方式
三、 使用乐观锁(Optimistic Locking)
实现步骤:
对比悲观锁:
总结
3. 数据库配置调整
4. 应用程序处理
6.预防死锁的最佳实践
7.总结
1.什么是数据库死锁?
死锁是指两个或多个事务在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力干涉,这些事务都无法继续执行。
简单来说,就像两个人过独木桥:
- 甲从东向西走,乙从西向东走
- 两人在桥中间相遇,都不愿意后退
- 结果两人都无法继续前进
下面是一个典型的数据库死锁场景的时序图,展示了两个事务如何相互等待对方持有的锁,从而导致死锁:
- 初始阶段:两个事务分别开始执行
- 获取锁阶段:
-
- 事务A获取表1中id=1记录的排他锁(X锁)
- 事务B获取表2中id=2记录的排他锁(X锁)
- 请求冲突阶段:
-
- 事务A尝试获取表2中id=2记录的锁,但该锁已被事务B持有
- 事务B尝试获取表1中id=1记录的锁,但该锁已被事务A持有
- 死锁形成:两个事务相互等待对方释放资源,形成循环等待
- 死锁解决:数据库死锁检测机制检测到死锁,选择回滚其中一个事务(通常选择回滚代价较小的事务)
2.死锁产生的必要条件
- 互斥条件:资源每次只能被一个进程使用
- 请求与保持条件:进程已经持有至少一个资源,但又请求新的资源
- 不剥夺条件:已分配的资源不能从相应进程中被强制剥夺
- 循环等待条件:多个进程形成头尾相接的循环等待资源关系
3.死锁产生的原因
以下是导致死锁的几个主要具体原因:
1. 多个事务之间的循环等待
这是最经典、最容易理解的死锁场景。两个或更多事务互相等待对方已持有的锁。
场景示例:
- 事务A:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
(持有id=1的行锁X锁) - 事务B:
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
(持有id=2的行锁X锁) - 事务A:
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
(尝试获取id=2的X锁,被事务B阻塞,进入等待) - 事务B:
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
(尝试获取id=1的X锁,被事务A阻塞,进入等待)
结果:事务A等待事务B,事务B又在等待事务A,形成循环等待,死锁发生。
根本原因:事务对资源的请求顺序不一致。如果所有事务都约定先操作id小的再操作id大的,那么死锁就不会发生(只会发生锁等待)。
2. 表级锁与行级锁混合(意向锁的存在)
InnoDB 支持多种粒度的锁(表锁、行锁)。为了协调不同粒度锁之间的关系,引入了意向锁(Intention Locks)。
- 意向共享锁 (IS):事务打算给某些行加 S锁。
- 意向排他锁 (IX):事务打算给某些行加 X锁。
规则是:
- 事务在获取一行的 S锁 前,必须先获取表的 IS锁(或更强的锁)。
- 事务在获取一行的 X锁 前,必须先获取表的 IX锁。
死锁场景示例(混合锁冲突):
- 事务A:
-
LOCK TABLES t WRITE;
-- 成功获取了表t
的写锁(表级X锁)
- 事务B:
-
SELECT * FROM t WHERE id = 1 FOR UPDATE;
-- 需要先获取表t
的 IX锁,但IX锁与事务A持有的X锁互斥,因此事务B被阻塞,等待事务A释放表锁。
- 事务A:
-
- 在持有表锁的情况下,也想执行:
SELECT * FROM t WHERE id = 1 FOR UPDATE;
- 但这条语句需要获取行锁,而获取行锁前需要先获取表的 IX锁。
- 然而,表的 IX锁 请求会与事务A自己已经持有的 X锁 产生冲突(根据锁兼容矩阵,X锁与自己请求的IX锁不兼容)。
- 在持有表锁的情况下,也想执行:
结果:事务A在等待自己持有的资源(它自己阻塞了自己),而事务B在等待事务A。虽然这不是典型的循环等待,但InnoDB也能检测到这种死锁,会回滚其中一个事务。
根本原因:显式的表锁(LOCK TABLES ...
)和 InnoDB 自己的行级锁(意向锁机制)混合使用,导致了复杂的锁冲突。
3. 间隙锁(Gap Lock)与临键锁(Next-Key Lock)导致的死锁
这是 MySQL 在 REPEATABLE-READ
隔离级别下特有且非常常见的死锁原因。间隙锁的存在不仅是为了防止幻读,也极大地增加了死锁的概率,因为它会锁定不存在的范围。
场景示例(并发插入导致的死锁):假设表 t
有索引列 col
,现有值:5, 10, 15。
- 事务A:
-
SELECT * FROM t WHERE col = 7 FOR UPDATE;
- 因为
7
不存在,为了防止幻读,它会锁定(5, 10)
这个间隙(Gap Lock)。
- 事务B:
-
SELECT * FROM t WHERE col = 7 FOR UPDATE;
- 同样,它也会请求对间隙
(5, 10)
加 Gap Lock。 - Gap Lock 之间是兼容的(允许多个事务锁定同一个间隙),所以事务B不会阻塞,成功加锁。
- 事务A:
-
INSERT INTO t (col) VALUES (7);
-- 插入操作需要获取插入意向锁(Insert Intention Lock)。- 插入意向锁与已存在的 Gap Lock 互斥。事务A需要等待事务B释放它持有的
(5,10)
的 Gap Lock。
- 事务B:
-
INSERT INTO t (col) VALUES (7);
-- 同样,它也需要获取插入意向锁。- 这个插入意向锁与事务A持有的 Gap Lock 互斥。因此事务B需要等待事务A释放Gap Lock。
结果:事务A在等待事务B,事务B又在等待事务A,形成死锁。
根本原因:两个事务都持有一个共享的间隙锁(Gap Lock),然后都尝试在该间隙内插入数据,而插入操作所需的锁与间隙锁互斥,导致了循环等待。
4. 不同索引访问路径导致的锁冲突
当一个表有多个索引时,一条SQL语句可能通过不同的索引来定位数据。加锁是加在索引记录上的,因此不同的索引访问路径可能导致意想不到的加锁顺序,从而引发死锁。
场景示例:假设表 users
有:
- 主键索引:
id
- 唯一索引:
username
- 普通索引:
status
现有数据:(id=1, username='alice', status='active')
- 事务A:
-
UPDATE users SET status = 'inactive' WHERE username = 'alice';
- 执行计划:使用
username
唯一索引找到记录,先锁定了username
索引上的 ‘alice’ 条目,然后根据主键值id=1
回表去锁主键索引上的id=1
记录。
- 事务B:
-
UPDATE users SET username = 'bob' WHERE id = 1;
- 执行计划:使用主键索引
id
找到记录,先锁定了主键索引上的id=1
记录,然后要去更新username
索引,需要锁定username
索引上旧的 ‘alice’ 条目(用于删除)和新的 ‘bob’ 条目(用于插入)。
加锁时序可能如下:
- 时间点1:事务A锁定了
username
索引上的alice
。 - 时间点2:事务B锁定了主键索引上的
id=1
。 - 时间点3:事务A试图去锁主键索引上的
id=1
(回表),但被事务B阻塞。 - 时间点4:事务B试图去锁
username
索引上的alice
(要删除它),但被事务A阻塞。
结果:循环等待,死锁发生。
根本原因:两个事务通过不同的索引路径访问同一条数据,导致了加锁顺序不一致(事务A先锁二级索引再锁主键索引,事务B先锁主键索引再锁二级索引)。
总结与启示
死锁原因 | 核心机制 | 预防策略 |
循环等待 | 事务对资源的请求顺序不一致 | 统一资源访问顺序(如按主键ID排序操作) |
混合锁冲突 | 意向锁与显式表锁的复杂交互 | 避免使用 ,依赖 InnoDB 的行级锁 |
间隙锁冲突 |
下,间隙锁与插入意向锁互斥 | 考虑使用 隔离级别(禁用间隙锁);尽快提交事务 |
不同索引路径 | 通过不同索引访问数据,加锁顺序不同 | 优化索引设计;让查询尽量使用相同的索引;保持事务简短 |
MySQL 死锁的本质是并发事务对锁资源的竞争形成了一个环路。InnoDB 的死锁检测机制会主动扫描这种环路,并选择回滚其中代价最小的事务(通常是修改数据量最少的事务)来解开死锁。
4.如何排查数据库死锁
4.1. 开启死锁日志记录
MySQL配置:
-- 查看当前死锁配置
SHOW VARIABLES LIKE '%innodb_print_all_deadlocks%';-- 开启死锁日志记录(临时)
SET GLOBAL innodb_print_all_deadlocks = 1;-- 在配置文件中永久开启
[mysqld]
innodb_print_all_deadlocks = 1
4.2. 监控死锁信息
查看最近死锁信息(MySQL):
SHOW ENGINE INNODB STATUS\G;
在返回结果中查找"LATEST DETECTED DEADLOCK"部分,这里会详细记录死锁发生的时间、涉及的事务和SQL语句。
4.3. 分析死锁日志
一个典型的死锁日志包含以下关键信息:
LATEST DETECTED DEADLOCK
------------------------
2023-10-15 10:34:56 0x7f8e2c34b700
*** (1) TRANSACTION:
TRANSACTION 123456, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 100, OS thread handle 139887, query id 1000 localhost root updating
UPDATE table1 SET column1 = 'value' WHERE id = 1*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 100 page no 10 n bits 80 index PRIMARY of table `test`.`table1` trx id 123456 lock_mode X locks rec but not gap
Record lock, heap no 10 PHYSICAL RECORD: n_fields 5; compact format; info bits 0*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 100 page no 11 n bits 80 index idx_column of table `test`.`table1` trx id 123456 lock_mode X locks rec but not gap waiting
Record lock, heap no 11 PHYSICAL RECORD: n_fields 2; compact format; info bits 0*** (2) TRANSACTION:
TRANSACTION 123457, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 101, OS thread handle 139888, query id 1001 localhost root updating
UPDATE table1 SET column1 = 'value2' WHERE id = 2*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 100 page no 11 n bits 80 index idx_column of table `test`.`table1` trx id 123457 lock_mode X locks rec but not gap
Record lock, heap no 11 PHYSICAL RECORD: n_fields 2; compact format; info bits 0*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 100 page no 10 n bits 80 index PRIMARY of table `test`.`table1` trx id 123457 lock_mode X locks rec but not gap waiting*** WE ROLL BACK TRANSACTION (2)
4.4. 使用性能Schema监控(MySQL 5.6+)
-- 查看当前锁信息
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;-- 查看最近死锁事件
SELECT * FROM performance_schema.events_transactions_current;
4.5. 使用sys Schema快速诊断(MySQL 5.7+)
-- 查看当前锁等待情况
SELECT * FROM sys.innodb_lock_waits;-- 查看最近的死锁信息
SELECT * FROM sys.schema_table_lock_waits;
5.解决死锁的常用方法
1. 事务设计优化
核心思想:打破死锁产生的四个必要条件
事务设计优化主要是为了打破“请求与保持”和“循环等待”这两个条件。
一、 保持事务简短且高效
这是最重要的一条原则。事务运行的时间越短,持有锁的时间就越短,与其他事务发生冲突的窗口期也就越短。
如何实现:
- 事前准备:在事务开始之前,做好所有必要的计算、验证和数据准备。不要在事务内进行复杂的业务逻辑计算、远程API调用或等待用户输入。
- 反例:
@Transactional
public void processOrder(Order order) {// 1. 计算税费、运费(耗时操作)calculateTaxAndShipping(order); // ❌ 不要在事务内做!// 2. 更新库存inventoryService.reduceStock(order.getItems());// 3. 创建订单orderDao.insert(order);// 4. 调用支付网关(网络IO)paymentService.callGateway(order); // ❌ 绝对不要在事务内做网络调用!
}
-
- 正例:
// 在事务外准备所有数据
Order processedOrder = calculateTaxAndShipping(order); // ✅
PaymentRequest request = buildPaymentRequest(processedOrder); // ✅@Transactional
public void processOrder(Order processedOrder) {// 事务内只包含数据库操作inventoryService.reduceStock(processedOrder.getItems());orderDao.insert(processedOrder);
}
// 事务提交后,再调用支付网关
paymentService.callGateway(request); // ✅
- 分批处理:对于需要更新大量数据的操作,采用分批处理的方式,每批一个独立的小事务,而不是一个巨大的长事务。
二、 严格遵守统一的访问顺序
这是避免循环等待最直接的方法。如果所有事务都以相同的顺序访问数据库对象(表、行),就不可能产生循环等待。
如何实现:
- 制定规范:在项目架构层面,规定所有业务操作必须按照某种顺序访问资源。
- 例如:在任何业务场景下,如果需要同时更新
users
表和orders
表,都必须先访问users
表,再访问orders
表。
- 代码审查:通过代码审查确保所有开发者都遵守这一规范。
- 示例对比:
-
- 死锁场景:
- 如果同时执行,A锁住了id=1,B锁住了id=2,然后它们分别尝试去锁对方已经锁住的id,死锁发生。
- 事务B:
UPDATE accounts SET balance = balance - 50 WHERE user_id = 2;
→UPDATE accounts SET balance = balance + 50 WHERE user_id = 1;
- 事务A:
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
→UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
- 死锁场景:
-
- 优化后(固定顺序):
- 事务B:也想操作id=2和id=1。但它会先尝试获取id=1的锁,但此时id=1的锁已被事务A持有,因此事务B会等待事务A释放id=1的锁。等待结束后,再继续执行。避免了循环等待,变成了简单的锁等待。
- 事务A:先操作id=1,再操作id=2。
- 规定:始终先操作user_id小的账户。
- 优化后(固定顺序):
三、 基于版本的乐观锁(Optimistic Locking)
悲观锁是“先取锁,再操作”,而乐观锁是“直接操作,在提交时检查是否冲突”。它避免了长时间持有锁,非常适合读多写少的场景。
如何实现:
为表增加一个版本号(version
)字段或时间戳字段。
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;
- 读取数据:同时取出数据和版本号。
SELECT stock, version FROM products WHERE id = 100;
-- 假设读出的 stock = 10, version = 5
- 执行业务逻辑:在内存中计算新的库存值。
- 更新数据:更新时带上版本号作为条件。
UPDATE products
SET stock = 9, version = version + 1
WHERE id = 100 AND version = 5;
- 检查结果:
-
- 如果受影响行数(affected rows)为 1,说明更新成功,没有冲突。
- 如果为 0,说明在本次操作过程中,版本号已被其他事务修改(数据已脏),此时应在应用层进行重试或抛出异常。
优点:根本避免了悲观锁带来的死锁问题。缺点:需要在应用层处理更新失败的情况,不适合写冲突非常频繁的场景。
四、 精细化锁的粒度:尽量使用行锁
确保你的SQL语句和索引设计能够有效地使用行级锁,而不是退化为表锁。
如何实现:
- 为
WHERE
条件建立高效索引:如果UPDATE
语句的WHERE
字段没有索引,InnoDB无法定位到具体行,就只能升级为表锁,极大增加死锁概率。 - 避免锁升级:避免一次性更新大量数据,这可能会导致锁升级(Lock Escalation)。
五、 总结与实战 checklist
当你设计一个事务时,可以按以下清单检查:
- [ ] 事务是否足够短? 是否移除了不必要的RPC调用、复杂计算和IO操作?
- [ ] 访问顺序是否统一? 多个表或多次行操作,是否所有可能并发的事务都遵循相同的访问顺序?
- [ ] 能否用乐观锁替代? 当前业务场景是否适合使用
version
字段进行乐观并发控制? - [ ] SQL是否高效?
UPDATE
和DELETE
语句的WHERE
条件是否都有索引,确保使用的是行锁? - [ ] 是否有重试机制? 对于不可避免的少量死锁或乐观锁冲突,是否在应用层设置了重试机制?(注意:重试是“治标”的容错手段,配合前几点“治本”策略使用效果最佳)
通过以上这些事务设计层面的优化,你能从源头上极大地降低死锁发生的概率,从而构建出更加健壮和高并发的高可用系统。
2. SQL优化
### 核心思想
SQL优化的目标是从单个SQL语句的层面减少锁的竞争强度、缩短锁的持有时间、缩小锁的影响范围,从而降低死锁的概率。
一、 减少锁定范围:索引是关键
原理:数据库加锁的基本单位是索引项。如果没有合适的索引,SQL语句可能无法精确定位数据,从而导致锁升级(例如,从行锁升级为表锁),极大地增加锁冲突和死锁的可能性。
1. 为 WHERE
子句和 UPDATE
条件建立索引
这是最基本也是最有效的一条。确保你的查询和更新语句都能高效地使用索引。
反例(致命):
- UPDATE orders SET status = 'shipped' WHERE customer_id = 450 AND status = 'processing';
-
- 问题:如果
(customer_id, status)
上没有联合索引,数据库为了找到所有status='processing'
的记录,可能被迫进行全表扫描。在全表扫描时,InnoDB会给所有扫描过的行加上锁(即使最终不满足条件),甚至会直接在整个表上加锁(如果优化器认为全表扫描代价更低)。这几乎是一场并发灾难。
- 问题:如果
- 正例:
-- 先确保有合适的索引
ALTER TABLE orders ADD INDEX idx_customer_status (customer_id, status);-- 现在同样的UPDATE语句只会锁定符合条件的行
UPDATE orders SET status = 'shipped' WHERE customer_id = 450 AND status = 'processing';
2. 使用主键或唯一索引进行更新
主键和唯一索引可以最精确地定位到一行数据,锁冲突最小。
- 最佳实践:尽量使用唯一性条件进行更新。
-- 好:直接使用主键定位,锁范围最小
UPDATE products SET price = 19.99 WHERE id = 1001;-- 不好:使用非唯一索引条件,可能会锁定多行,甚至因为间隙锁导致更多冲突
UPDATE products SET price = 19.99 WHERE name = 'Awesome Product';
-- 即使name有索引,如果'Awesome Product'不唯一,也会锁定所有该名称的行。
3. 理解并小心间隙锁(Gap Lock)
间隙锁是InnoDB在可重复读(Repeatable Read) 隔离级别下为了防止幻读而引入的机制。它会锁定一个索引范围之间的“间隙”,即使这些间隙中不存在数据。
- 场景:假设表
users
的主键 id 有 1, 5, 10 三条记录。
-- 事务A
SELECT * FROM users WHERE id BETWEEN 5 AND 10 FOR UPDATE;
这条语句不仅会锁住 id=5 和 id=10 的现有记录,还会锁住 (1,5), (5,10), (10, +∞) 这些间隙,防止其他事务插入 id=2, 3, 4, 6, 7, 8, 9, 11 等记录。
- 优化策略:
-
- 使用读已提交(RC)隔离级别:在
READ-COMMITTED
隔离级别下,间隙锁会被禁用,从而减少锁冲突。但需要评估业务是否能接受幻读(Phantom Read)。 - 避免使用范围条件:如果业务允许,尽量使用等值查询。
- 确保索引设计合理:糟糕的索引设计会让查询匹配到更大的范围,导致更宽的间隙被锁定。
- 使用读已提交(RC)隔离级别:在
二、 避免热点更新:使用CAS(Compare-and-Swap)
原理:当大量并发事务同时更新同一行数据时(例如商品库存、计数器、点赞数),就会形成“热点”。传统的 UPDATE table SET value = value - 1
会在这一行上产生激烈的锁竞争。CAS方式将“计算”和“设置”合并为一个原子操作,在SQL层面完成,避免了在应用层计算时长时间持有锁。
传统方式 vs CAS方式
- 传统方式(易冲突):
-
SELECT stock FROM products WHERE id = 100;
(读取出 stock = 10)- 在应用内存中计算:
new_stock = 10 - 1 = 9
UPDATE products SET stock = 9 WHERE id = 100;
(长时间持有X锁,等待其他事务提交)
- CAS方式(推荐):
UPDATE products
SET stock = stock - 1
WHERE id = 100 AND stock > 0; -- CAS操作:在更新的同时检查条件
-
- 优势:
- 高效:数据库内部处理,速度极快。
- 安全:通过
WHERE stock > 0
避免了库存超卖。 - 原子性:读、计算、写在一条SQL中完成,极大缩短了锁的持有时间。
- 优势:
- 应用层判断:
int affectedRows = productMapper.decreaseStock(100, 1);
if (affectedRows == 0) {// 更新失败,库存可能不足或商品不存在throw new RuntimeException("库存扣减失败");
}
三、 使用乐观锁(Optimistic Locking)
乐观锁不是用数据库的悲观锁(FOR UPDATE
)来强制排队,而是通过一个版本号字段来检测数据在事务执行过程中是否被他人修改过。它本身不加锁,因此从根本上避免了死锁。
实现步骤:
表结构增加版本号字段:
- ALTER TABLE orders ADD COLUMN version INT DEFAULT 0;
- 更新数据时,将版本号作为条件:
-- 1. 先查询出当前数据和版本号
SELECT id, total_amount, version FROM orders WHERE id = 2001;
-- 假设查到 version = 5-- 2. 更新时,SET版本号+1,WHERE条件中带上旧的版本号
UPDATE orders
SET total_amount = 250.00, version = version + 1
WHERE id = 2001 AND version = 5;
- 检查更新结果:
-
- 如果这条
UPDATE
语句的受影响行数(affected rows)为 1,说明更新成功,没有冲突。 - 如果受影响行数为 0,说明在本次操作过程中,版本号已经不匹配(数据已经被其他事务修改过)。此时应在应用层进行重试(重新执行步骤1和2)或向用户返回错误。
- 如果这条
对比悲观锁:
- 悲观锁(Pessimistic Locking):
BEGIN;
SELECT * FROM orders WHERE id = 2001 FOR UPDATE; -- 立即加上X锁,其他事务被阻塞
-- ... 应用层计算 ...
UPDATE orders SET total_amount = 250.00 WHERE id = 2001;
COMMIT;
-
- 优点:保证强一致性。
- 缺点:并发性能低,有死锁风险。
- 乐观锁(Optimistic Locking):
- 优点:并发性能极高,无死锁风险。
- 缺点:需要处理更新失败的情况,适合读多写少、冲突不太频繁的场景。
总结
通过SQL优化避免死锁,本质上是让你的数据库操作更加“精准”和“迅速”:
策略 | 具体方法 | 效果 |
减少锁定范围 | 为查询条件添加合适索引 | 避免全表扫描,将锁精确到行 |
优先使用主键/唯一键更新 | 最小化锁冲突范围 | |
理解并谨慎使用范围查询 | 避免不可控的间隙锁 | |
避免热点更新 | 使用CAS( ) | 将计算下沉到数据库,极短时间持有锁 |
使用乐观锁 | 增加 字段,更新时校验 | 彻底不加锁,通过重试机制解决冲突 |
将这些SQL优化手段与之前提到的事务设计优化(如短事务、固定顺序)结合使用,就能构建出一个全方位抵御死锁的坚固系统。
3. 数据库配置调整
-- 增加锁超时时间(根据业务场景调整)
SET GLOBAL innodb_lock_wait_timeout = 120;-- 降低隔离级别(需评估业务影响)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
4. 应用程序处理
- 实现重试机制:捕获死锁异常后自动重试事务
- 使用分布式锁:在应用层控制并发访问
// Java示例:死锁重试机制
int retryCount = 0;
boolean success = false;
while (!success && retryCount < MAX_RETRY) {try {// 执行数据库操作executeTransaction();success = true;} catch (DeadlockLoserDataAccessException e) {retryCount++;if (retryCount >= MAX_RETRY) {throw e;}// 等待一段时间后重试Thread.sleep((long) (Math.random() * 100));}
}
6.预防死锁的最佳实践
- 统一访问顺序:所有业务代码按照相同顺序访问表
- 索引优化:确保查询使用合适的索引,减少锁定范围
- 事务精简:保持事务短小精悍,尽快提交
- 监控预警:设置死锁监控告警,及时发现和处理
- 压力测试:在高并发场景下测试应用,提前发现潜在死锁问题
7.总结
数据库死锁是高并发系统中的常见问题,通过合理的监控、分析和优化,可以大大降低死锁发生的频率和影响。关键在于:
- 建立完善的监控体系,及时发现死锁
- 深入分析死锁原因,找到根本问题
- 从事务设计、SQL优化、应用处理等多维度进行优化
- 建立预防机制,避免类似问题重复发生
希望本篇指南能帮助你在遇到数据库死锁问题时,能够快速定位并解决这些问题。
注意:不同数据库系统(MySQL、PostgreSQL、Oracle等)的死锁排查方法略有不同,但核心思路是一致的。在实际工作中请根据使用的数据库类型调整具体命令和方法。