问题背景

近日在开发一个分布式PT系统时,遇到一个典型的事务死锁问题。用户注册成功,但积分余额未创建,日志显示"Lock wait timeout exceeded"。追踪发现问题出在事务传播行为(propagation)设置不当。

问题代码

用户注册服务片段:

@Transactional
public UserDTO registerOpenUser(String username, String email, String password, String verificationCode) {
    // 验证并创建用户...
    userMapper.insert(user);
    
    // 创建Passkey...
    passkeyHistoryMapper.insert(passkeyHistory);
    
    // 创建积分余额(使用独立事务)
    userPointsService.createUserPointBalance(userId, new BigDecimal("0.00"));
    
    return userDTO;
}

积分服务实现:

@Transactional(propagation = Propagation.REQUIRES_NEW)  // 问题根源
public boolean createUserPointBalance(Long userId, BigDecimal initialPoints) {
    // 检查用户ID...
    // 创建积分余额...
    userPointsBalanceMapper.insert(pointsBalance);
    return true;
}

死锁形成过程

  1. 用户注册事务获取用户表行锁
  2. 调用积分服务,创建新独立事务
  3. 积分事务尝试访问用户相关资源,需要锁
  4. 外部事务等待内部事务完成,内部事务等待外部事务释放锁
  5. 50秒后锁等待超时,积分事务失败,但用户注册继续

事务传播行为解析

Spring提供七种事务传播行为,其中:

  • REQUIRED(默认):使用现有事务,没有则创建新事务
  • REQUIRES_NEW:总是创建新事务,挂起当前事务

当嵌套调用使用REQUIRES_NEW时:

  1. 外部事务被挂起
  2. 创建全新、独立的内部事务
  3. 内部事务完成后恢复外部事务

为何产生死锁?

  1. 资源争用:两个事务操作关联资源
  2. 事务隔离:MySQL默认使用REPEATABLE_READ隔离级别
  3. 锁持有:事务持有行锁直到提交
  4. 依赖循环
    • 外部事务持有用户表锁,等待内部事务完成
    • 内部事务需要验证用户ID,等待外部事务释放锁

解决方案

将积分服务的事务传播行为改为REQUIRED:

@Transactional(propagation = Propagation.REQUIRED)
public boolean createUserPointBalance(Long userId, BigDecimal initialPoints) {
    // 方法实现不变
}

改动后:

  1. 积分创建使用现有事务上下文
  2. 共享同一事务的锁集
  3. 操作在单一事务中执行
  4. 不会产生额外事务开销和资源争用

死锁发生时序图

sequenceDiagram participant Client as "客户端" participant UserSvc as "UserService <br>(REQUIRED)" participant UserTx as "用户注册事务" participant UserDB as "Users表" participant PasskeyDB as "Passkey表" participant PointsSvc as "UserPointsService <br>(REQUIRES_NEW)" participant PointsTx as "积分创建事务" participant PointsDB as "UserPointsBalance表" Client->>UserSvc: 调用registerOpenUser() activate UserSvc Note over UserSvc,UserTx: @Transactional开始 UserSvc->>UserTx: 开始用户注册事务 activate UserTx UserTx->>UserDB: 获取User表的行锁(检查用户名/邮箱是否存在) UserDB-->>UserTx: 返回结果 UserTx->>UserDB: 插入用户记录 UserDB-->>UserTx: 用户ID=12 UserTx->>PasskeyDB: 插入Passkey记录 PasskeyDB-->>UserTx: 成功 Note over UserSvc,PointsSvc: 调用积分服务创建积分记录 UserSvc->>PointsSvc: createUserPointBalance(userId=12) activate PointsSvc Note over PointsSvc,PointsTx: @Transactional(REQUIRES_NEW)开始 PointsSvc->>PointsTx: 创建新独立事务 activate PointsTx PointsTx->>PointsDB: 检查用户ID=12是否已有积分记录 PointsDB-->>PointsTx: 返回结果 Note over PointsTx,UserTx: 死锁产生: <br>两个事务都需要锁定与用户ID=12相关的资源 PointsTx--xPointsDB: 插入积分记录 (等待用户表锁释放) UserTx--xUserDB: 等待积分事务完成 (持有用户表锁) Note over PointsTx: 经过50秒锁等待超时 PointsTx-->>PointsSvc: 抛出锁等待超时异常 deactivate PointsTx PointsSvc-->>UserSvc: 传播异常 deactivate PointsSvc Note over UserSvc: 捕获异常但继续执行 UserTx->>UserTx: 提交用户和Passkey记录 deactivate UserTx UserSvc-->>Client: 返回用户注册成功(但无积分记录) deactivate UserSvc