一、死锁到底是个什么鬼
咱们先来打个比方。想象两个人在狭窄的走廊里迎面相遇,都坚持让对方先走,结果谁都不让谁,就这么僵持住了——这就是死锁的生动写照。在SqlServer里,当两个或多个事务互相等待对方释放锁资源时,就会发生这种"堵车"现象。
举个典型的死锁场景:
- 事务A锁定了表1,然后请求表2的锁
- 事务B同时锁定了表2,却请求表1的锁
- 两个事务就像两个固执的人,大眼瞪小眼谁也不让步
-- 事务1执行顺序
BEGIN TRAN
UPDATE 商品表 SET 库存=库存-1 WHERE 商品ID=1 -- 获取商品1的排他锁
WAITFOR DELAY '00:00:05' -- 模拟业务处理耗时
UPDATE 订单表 SET 状态='已付款' WHERE 订单ID=100 -- 尝试获取订单表的锁
COMMIT TRAN
-- 事务2执行顺序
BEGIN TRAN
UPDATE 订单表 SET 备注='加急' WHERE 订单ID=100 -- 获取订单表的排他锁
WAITFOR DELAY '00:00:03' -- 模拟业务处理耗时
UPDATE 商品表 SET 库存=库存+1 WHERE 商品ID=1 -- 尝试获取商品表的锁
COMMIT TRAN
二、如何发现死锁的蛛丝马迹
SqlServer其实挺贴心的,给我们准备了几种监控死锁的工具:
- 最快捷的方式就是查看SqlServer错误日志,死锁发生时系统会自动记录
-- 查询最近的死锁信息
EXEC sp_readerrorlog 0, 1, 'deadlock'
- 使用扩展事件(XEvent)实时监控,就像给数据库装了个行车记录仪
-- 创建死锁监控会话
CREATE EVENT SESSION [Deadlock_Monitor] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file(SET filename=N'Deadlock_Monitor')
GO
-- 启动监控
ALTER EVENT SESSION [Deadlock_Monitor] ON SERVER STATE = START
GO
- 老牌工具SQL Profiler虽然已经退役,但在某些老版本上还是可以凑合用
-- 使用Trace Flag捕获死锁图
DBCC TRACEON (1222, -1) -- 将死锁信息输出到错误日志
DBCC TRACEON (1204, -1) -- 另一种死锁记录格式
三、破解死锁的十八般武艺
遇到死锁别慌张,咱们有全套解决方案:
方案1:调整事务隔离级别
就像调节座椅高度一样,合适的隔离级别能减少锁冲突
-- 使用读已提交快照,减少阻塞
ALTER DATABASE 你的数据库
SET ALLOW_SNAPSHOT_ISOLATION ON
ALTER DATABASE 你的数据库
SET READ_COMMITTED_SNAPSHOT ON
方案2:统一访问顺序
让所有事务按照固定顺序访问表,就像交通规则靠右行驶
-- 修改事务代码,确保总是先访问商品表再访问订单表
CREATE PROCEDURE 下单流程
AS
BEGIN
BEGIN TRAN
-- 总是先操作商品表
UPDATE 商品表...
-- 然后操作订单表
UPDATE 订单表...
COMMIT TRAN
END
方案3:添加索引减少锁范围
就像把大马路划分成多个车道
-- 为常用查询条件添加索引
CREATE INDEX IX_订单表_状态 ON 订单表(状态)
INCLUDE (订单金额, 客户ID)
-- 使用覆盖索引减少锁升级
CREATE INDEX IX_商品表_库存 ON 商品表(库存)
INCLUDE (商品名称, 价格)
方案4:设置死锁优先级
让不太重要的事务主动让路
-- 设置事务优先级
BEGIN TRAN
SET DEADLOCK_PRIORITY LOW -- 本事务优先级较低
-- 业务逻辑...
COMMIT TRAN
四、实战中容易踩的坑
- 过度依赖NOLOCK提示:虽然能减少阻塞,但可能读到脏数据
-- 危险操作:可能读到未提交的数据
SELECT * FROM 订单表 WITH(NOLOCK) WHERE 订单ID=100
- 忽略锁升级:当锁数量超过阈值时,SqlServer会自动升级为表锁
-- 监控锁升级事件
DBCC TRACEON (1211, 1224, -1) -- 禁用锁升级(谨慎使用)
- 长事务问题:事务持续时间越长,死锁风险越高
-- 错误示范:包含用户交互的长事务
BEGIN TRAN
UPDATE 商品表...
-- 等待用户确认...(千万别这么做!)
COMMIT TRAN
- 忽略死锁重试机制:有时候自动重试就能解决问题
// C#代码示例:死锁重试逻辑
int retryCount = 0;
while(retryCount < 3)
{
try
{
using(var tran = connection.BeginTransaction())
{
// 执行SQL操作
tran.Commit();
break;
}
}
catch(SqlException ex) when (ex.Number == 1205) // 死锁错误码
{
retryCount++;
Thread.Sleep(100 * retryCount);
}
}
五、从预防到治理的全套方案
- 预防阶段:
- 定期检查缺失索引
- 分析执行计划中的警告
- 使用Database Tuning Advisor获取优化建议
- 监控阶段:
- 配置实时告警
- 建立基线性能指标
- 定期检查锁等待统计
-- 查询当前锁等待情况
SELECT
t.text AS 执行语句,
wt.wait_type AS 等待类型,
wt.wait_duration_ms AS 等待时长,
wt.resource_description AS 资源描述
FROM sys.dm_os_waiting_tasks wt
INNER JOIN sys.dm_exec_requests r ON wt.session_id = r.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE wt.wait_type LIKE 'LCK%'
- 治理阶段:
- 优化事务设计
- 重构问题查询
- 考虑应用层缓存
- 应急处理:
- 使用KILL命令终止阻塞源头
- 临时调整隔离级别
- 启用快照隔离
-- 终止阻塞会话
SELECT 阻塞会话ID = blocking_session_id, 被阻塞会话ID = session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id != 0
KILL 55 -- 终止指定会话
六、总结与最佳实践
经过这一番折腾,咱们可以得出几个金科玉律:
- 事务要短小精悍,别搞马拉松式长事务
- 访问顺序要统一,就像进出电梯先出后进
- 索引不是越多越好,但要确保关键查询有合适索引
- 监控要常态化,别等用户投诉才发现问题
- 适当使用乐观并发控制,特别是读多写少的场景
最后记住,死锁不可怕,可怕的是对死锁视而不见。只要掌握正确的工具和方法,就能让数据库运行得像高速公路一样畅通无阻。
评论