首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >MySQL死锁问题排查与解决:高并发场景下的库存扣减实战

MySQL死锁问题排查与解决:高并发场景下的库存扣减实战

原创
作者头像
远方诗人
发布2025-08-27 10:24:06
发布2025-08-27 10:24:06
2040
举报

技术环境

  • MySQL版本:8.0.28
  • 存储引擎:InnoDB
  • 事务隔离级别:REPEATABLE-READ(默认)
  • 应用框架:Spring Boot 2.7.4 + MyBatis 3.5.10
  • 业务场景:电商平台库存扣减,峰值QPS约800

Bug现象

在高并发压力测试期间,库存扣减操作偶发性失败,错误率约2.3%。日志中出现以下异常:

代码语言:txt
复制
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: 
Deadlock found when trying to get lock; try restarting transaction

应用层捕获到死锁异常后进行了事务回滚,但部分用户反馈"库存充足却提示下单失败"。

排查步骤

1. 确认死锁场景

首先开启MySQL死锁日志记录(需在my.cnf中配置):

代码语言:sql
复制
SET GLOBAL innodb_print_all_deadlocks = ON;

2. 分析死锁日志

从MySQL错误日志中提取到以下关键信息:

代码语言:txt
复制
*** (1) TRANSACTION:
TRANSACTION 123456789, ACTIVE 0 sec updating rows
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 11264, 3 row lock(s), undo log entries 1
MySQL thread id 111, OS thread handle 0x7f8b1c0b1700, query id 2222 10.0.0.1 user updating
UPDATE product_stock SET quantity = quantity - 1 WHERE product_id = 10086 AND quantity > 0

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 333 page no 4 n bits 80 index `idx_product_id` of table `db`.`product_stock` trx id 123456789 lock_mode X locks rec but not gap

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 333 page no 4 n bits 80 index `idx_product_id` of table `db`.`product_stock` trx id 123456789 lock_mode X locks rec but not gap waiting

*** (2) TRANSACTION:
TRANSACTION 123456790, ACTIVE 0 sec updating rows
mysql tables in use 1, locked 1
5 lock struct(s), heap size 11264, 3 row lock(s), undo log entries 1
MySQL thread id 112, OS thread handle 0x7f8b1c0b2700, query id 2223 10.0.0.2 user updating
UPDATE product_stock SET quantity = quantity - 1 WHERE product_id = 10086 AND quantity > 0

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 333 page no 4 n bits 80 index `idx_product_id` of table `db`.`product_stock` trx id 123456790 lock_mode X locks rec but not gap

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 333 page no 4 n bits 80 index `idx_product_id` of table `db`.`product_stock` trx id 123456790 lock_mode X locks rec but not gap waiting

3. 表结构分析

检查相关表结构:

代码语言:sql
复制
CREATE TABLE `product_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(20) NOT NULL,
  `quantity` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4. 业务代码分析

原始扣减库存的代码逻辑:

代码语言:java
复制
@Transactional(rollbackFor = Exception.class)
public boolean reduceStock(Long productId, Integer quantity) {
    // 1. 检查库存是否充足
    Integer currentStock = stockMapper.getStockByProductId(productId);
    if (currentStock < quantity) {
        throw new RuntimeException("库存不足");
    }
    
    // 2. 更新库存
    int rows = stockMapper.reduceStock(productId, quantity);
    return rows > 0;
}

对应的Mapper SQL:

代码语言:xml
复制
<update id="reduceStock">
    UPDATE product_stock 
    SET quantity = quantity - #{quantity}
    WHERE product_id = #{productId} 
    AND quantity >= #{quantity}
</update>

问题根因

死锁产生原因分析

  1. 间隙锁冲突:虽然表有唯一索引,但在REPEATABLE-READ隔离级别下,UPDATE语句仍会获取间隙锁
  2. 执行顺序问题:两个事务同时执行时:
    • 事务A:获取product_id=10086的行锁
    • 事务B:获取product_id=10086的行锁
    • 事务A:尝试获取间隙锁,但被事务B持有
    • 事务B:尝试获取间隙锁,但被事务A持有
  3. 检查然后更新:先SELECT检查再UPDATE更新的模式存在时间差,并发时数据一致性无法保证

解决方案

方案一:优化SQL语句(采用)

修改UPDATE语句,将库存检查合并到UPDATE中:

代码语言:xml
复制
<update id="reduceStock">
    UPDATE product_stock 
    SET quantity = quantity - #{quantity}
    WHERE product_id = #{productId} 
    AND quantity >= #{quantity}
</update>

业务代码调整为:

代码语言:java
复制
@Transactional(rollbackFor = Exception.class)
public boolean reduceStock(Long productId, Integer quantity) {
    int rows = stockMapper.reduceStock(productId, quantity);
    if (rows == 0) {
        // 更新失败说明库存不足
        throw new RuntimeException("库存不足");
    }
    return true;
}

方案二:使用悲观锁(备用)

代码语言:java
复制
@Transactional(rollbackFor = Exception.class)
public boolean reduceStockWithLock(Long productId, Integer quantity) {
    // 1. 加锁查询
    ProductStock stock = stockMapper.selectForUpdate(productId);
    if (stock.getQuantity() < quantity) {
        throw new RuntimeException("库存不足");
    }
    
    // 2. 更新库存
    stockMapper.updateStock(stock.getId(), stock.getQuantity() - quantity);
    return true;
}

对应的SQL:

代码语言:xml
复制
<select id="selectForUpdate" resultType="ProductStock">
    SELECT * FROM product_stock 
    WHERE product_id = #{productId} 
    FOR UPDATE
</select>

方案三:使用乐观锁(适合低冲突场景)

增加版本号字段:

代码语言:sql
复制
ALTER TABLE product_stock ADD COLUMN version int NOT NULL DEFAULT 0;
代码语言:java
复制
public boolean reduceStockWithVersion(Long productId, Integer quantity) {
    int retryTimes = 3;
    for (int i = 0; i < retryTimes; i++) {
        ProductStock stock = stockMapper.getStock(productId);
        if (stock.getQuantity() < quantity) {
            throw new RuntimeException("库存不足");
        }
        
        int rows = stockMapper.updateStockWithVersion(
            productId, quantity, stock.getVersion());
        if (rows > 0) {
            return true;
        }
    }
    throw new RuntimeException("并发更新失败,请重试");
}

避坑总结

  1. 避免先查后改:在高并发场景下,SELECT+UPDATE模式极易产生竞态条件
  2. 统一更新条件:将业务判断条件整合到UPDATE的WHERE子句中,利用数据库原子性保证一致性
  3. 控制事务粒度:尽量减少事务持有锁的时间,避免在事务中包含非必要的业务逻辑
  4. 索引优化:确保UPDATE语句的WHERE条件能够命中索引,减少锁范围
  5. 重试机制:对于死锁等临时性错误,实现适当的重试机制
  6. 监控告警:建立数据库死锁监控,及时发现和处理问题

后续优化

实施解决方案后,我们进一步采取了以下措施:

  1. 引入库存预占机制:将库存扣减分为预占和实际扣减两个阶段
  2. 数据库连接池优化:调整连接池参数,减少连接等待时间
  3. 业务削峰:对秒杀等场景使用队列缓冲请求
  4. 压力测试:定期进行高并发压力测试,提前发现潜在问题

经过以上优化,死锁问题完全解决,系统在同等压力下运行稳定,库存扣减错误率降至0.001%以下。

启示:MySQL死锁问题往往源于并发控制不当,需要从数据库设计、SQL编写、业务逻辑等多个层面进行综合考量才能有效解决。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 技术环境
  • Bug现象
  • 排查步骤
    • 1. 确认死锁场景
    • 2. 分析死锁日志
    • 3. 表结构分析
    • 4. 业务代码分析
  • 问题根因
    • 死锁产生原因分析
  • 解决方案
    • 方案一:优化SQL语句(采用)
    • 方案二:使用悲观锁(备用)
    • 方案三:使用乐观锁(适合低冲突场景)
  • 避坑总结
  • 后续优化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档