问题背景
近日在开发一个分布式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;
}
死锁形成过程
- 用户注册事务获取用户表行锁
- 调用积分服务,创建新独立事务
- 积分事务尝试访问用户相关资源,需要锁
- 外部事务等待内部事务完成,内部事务等待外部事务释放锁
- 50秒后锁等待超时,积分事务失败,但用户注册继续
事务传播行为解析
Spring提供七种事务传播行为,其中:
- REQUIRED(默认):使用现有事务,没有则创建新事务
- REQUIRES_NEW:总是创建新事务,挂起当前事务
当嵌套调用使用REQUIRES_NEW时:
- 外部事务被挂起
- 创建全新、独立的内部事务
- 内部事务完成后恢复外部事务
为何产生死锁?
- 资源争用:两个事务操作关联资源
- 事务隔离:MySQL默认使用REPEATABLE_READ隔离级别
- 锁持有:事务持有行锁直到提交
- 依赖循环:
- 外部事务持有用户表锁,等待内部事务完成
- 内部事务需要验证用户ID,等待外部事务释放锁
解决方案
将积分服务的事务传播行为改为REQUIRED:
@Transactional(propagation = Propagation.REQUIRED)
public boolean createUserPointBalance(Long userId, BigDecimal initialPoints) {
// 方法实现不变
}
改动后:
- 积分创建使用现有事务上下文
- 共享同一事务的锁集
- 操作在单一事务中执行
- 不会产生额外事务开销和资源争用
评论区