死锁定义

死锁是指在并发事务中,多个事务彼此持有某些资源,并同时又在等待对方持有的资源,从而导致循环等待,最终所有相关事务都无法继续执行的状态。

  • 简单理解:两个事务互相卡住,你等我释放,我等你释放,结果谁也走不下去。

必要条件

死锁的发生通常需要以下四个条件同时满足(任意一个不成立就不会死锁):

  • 互斥条件:资源同一时间只能被一个事务占有(如表锁、行锁)。
  • 不可抢占条件:已经分配给事务的资源,不能被强制剥夺,只能事务自己释放。
  • 部分分配条件(请求并保持条件):事务已经持有了一部分资源,但还在申请新的资源。
  • 循环等待条件:存在一个事务等待队列,形成了环路:T1 等 T2 的资源,T2 等 T3 的资源,T3 又等 T1 的资源。

表锁死锁

产生的原因: 当多个数据库用户并发访问资源时,如果请求顺序不当就可能产生死锁。下面是一个典型的死锁场景示例:

  1. 用户A的事务流程:

    • 首先执行 SELECT * FROM tableA FOR UPDATE(获取表A的排他锁)
    • 然后尝试执行 SELECT * FROM tableB FOR UPDATE(等待获取表B的锁)
  2. 同时用户B的事务流程:

    • 先执行 SELECT * FROM tableB FOR UPDATE(获取表B的排他锁)
    • 然后尝试执行 SELECT * FROM tableA FOR UPDATE(等待获取表A的锁)

此时系统进入死锁状态:

  • 用户A持有tableA的锁,等待获取tableB的锁
  • 用户B持有tableB的锁,等待获取tableA的锁
  • 两个事务互相等待对方释放资源,形成循环等待

常见应用场景

  • 电商系统中,用户A在修改订单表时需要同时更新库存表
  • 用户B在修改库存表时需要同时更新订单表
  • 如果两个操作同时发生且顺序不一致就可能死锁

解决方案

  1. 资源排序法

    • 为所有资源(如表)定义固定的访问顺序
    • 例如给所有表编号,操作时严格按照编号从小到大顺序访问
    • 示例:若有表A(id=1)、B(id=2)、C(id=3),所有事务必须先访问A,再B,最后C
  2. 锁超时机制

    • 设置合理的锁等待超时时间(如5秒)
    • 超时后自动回滚当前事务并重试
    • SQL Server示例:SET LOCK_TIMEOUT 5000
  3. 事务优化建议

    • 尽量缩小事务范围
    • 避免在事务中包含用户交互
    • 将大事务拆分为多个小事务
    • 使用乐观锁替代悲观锁

行级死锁

产生的原因1: 如果事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等价于表级锁),多个这样的事务执行之后,很容易出产生死锁和阻塞,最终应用系统越来越慢,从而进一步加重阻塞和死锁。

解决方案1: SQL语句中不要使用太复杂的关联多表的查询,使用 EXPLAIN 执行计划对SQL语句进行分析,对于有全表扫描和全表锁定的SQL,建立相应的索引进行优化。

产生的原因2: 两个事务都想拿到对方持有的锁,互相等待,于是产生了死锁。

解决方案2: 在同一个事务中,尽可能做到一次锁定所需要的所有资源。按照id对资源进行排序,然后按照顺序进行处理。

共享锁转排他锁

产生原因: 事务A查询一条记录,然后更新该记录,此时事务B也更新该条记录,这时事务B的排他锁由于事务A有共享锁,必须等A释放共享锁后才可以获取,只能排队等待。

事务A:

-- 共享锁1
SELECT * FROM dept WHERE deptno = 1 LOCK IN SHARE MODE;

-- 排他锁3
UPDATE dept SET dname='java' WHERE deptno=1;

事务B:

-- 由于1有共享锁 没法获取其他锁 需要等待
UPDATE dept SET dname='java' WHERE deptno=1

解决方案

  • 对于按钮控件,点击立刻失效,不让用户重复点击,避免同一个时间发生多条记录操作。
  • 使用乐观锁进行控制,乐观锁避免了长事务中的数据库加锁开销,大大提升了并发量下的系统性能。

死锁排查

查看死锁

通过以下指令查看近期死锁的日志:

SHOW ENGINE INNODB STATUS\G;

查看锁状态

要深入分析MySQL InnoDB引擎的行锁争夺情况,可以通过以下命令查看关键状态变量:

SHOW STATUS LIKE 'innodb_row_lock%';

这些状态变量提供了以下重要信息:

  1. innodb_row_lock_current_waits(当前等待锁的数量)

    • 实时显示系统中正在等待行锁的会话数量
  2. innodb_row_lock_time(总锁定时间)

    • 累计统计自数据库启动以来所有行锁等待的总时间(毫秒)
  3. innodb_row_lock_time_avg(平均等待时间)

    • 每次行锁等待的平均耗时(毫秒)
    • 健康指标:通常应保持在100ms以下,超过500ms需重点关注
  4. innodb_row_lock_time_max(最长等待时间)

    • 记录系统启动以来单个事务等待行锁的最长时间
  5. innodb_row_lock_waits(总等待次数)

    • 累计发生的行锁等待总次数

避免死锁的常见方法

1. 保持一致的加锁顺序

  • 主键排序法:当需要更新多行数据时,按照主键值的升序或降序统一进行加锁。
  • 表级排序法:涉及多表操作时,约定固定的表访问顺序(如按表名字典序)。
  • 资源分级法:将数据库资源分级(如表、页、行),按照从高到低或从低到高的固定顺序加锁。

2. 尽量缩短事务时间

  • 减少交互:避免在事务中包含用户交互、网络请求等耗时操作
  • 预处理数据:在事务外完成数据准备和校验工作
  • 批量操作:使用批量更新替代循环单条更新
  • 及时提交:完成核心操作后立即提交,不要保留不必要的事务

3. 合理选择事务隔离级别

  • READ UNCOMMITTED:无锁读取,但可能读到脏数据
  • READ COMMITTED:大多数数据库默认级别,只锁定正在读取的行
  • REPEATABLE READ:保证事务内多次读取结果一致(MySQL默认)
  • SERIALIZABLE:最严格的隔离级别,锁开销最大

4. 使用更合适的索引

  • 避免全表扫描:确保WHERE条件都能命中索引,减少锁升级为表锁的风险
  • 精准索引:使用覆盖索引减少回表操作
  • 索引选择性:高选择性的索引能减少锁定的数据范围
  • 注意索引失效:避免在索引列上使用函数、类型转换等导致索引失效的操作

5. 分解大事务

  • 功能拆分:将一个大事务按功能拆分为多个独立小事务
  • 数据分片:按数据维度拆分
  • 定时提交:大数据量操作时每处理N条记录提交一次
  • 补偿机制:对于必须保持原子性的操作,实现补偿事务处理部分失败的情况

6. 显式控制加锁顺序

  • 预先锁定:使用SELECT … FOR UPDATE在事务开始时锁定所有必要资源
  • 锁升级策略:先获取共享锁,需要时再升级为排他锁
  • 超时机制:设置锁获取超时时间
  • 死锁检测:实现应用层的死锁检测和重试机制
  • 锁粒度控制:根据场景选择行锁、页锁或表锁
SELECT * FROM table LOCK IN SHARE MODE;

排他锁(行级锁-写锁)

SELECT * FROM employees WHERE id = 1 FOR UPDATE;

特性

  1. 排他性
  2. 独占访问
  3. 阻塞其他操作

乐观锁

乐观锁是一种并发控制机制,需要开发者自行设计实现。

实现方式

版本号机制

-- 读取
SELECT id, name, version FROM products WHERE id = 1;

-- 更新
UPDATE products
SET name = '新商品名称', version = version + 1
WHERE id = 1 AND version = 1;

适用场景

  • 读多写少的应用
  • 并发冲突概率较低

变体实现

  • 时间戳方式
  • 条件更新

注意事项

  • 需要处理更新失败的情况
  • 不适合高并发写场景