Spring 声明式事务的三个坑
用了这么久 @Transactional,你确定事务真的生效了吗?
说实话,据我观察,至少 20% 的业务代码的事务都没处理正确。平时跑得好好的,一到高并发或者出问题的时候,数据不一致的 bug 就冒出来了。
今天聊三个最常见的坑。
坑一:事务根本没生效
private 方法上的 @Transactional
java
@Service
public class UserService {
public int createUser(String name) {
try {
this.createUserPrivate(new UserEntity(name));
} catch (Exception ex) {
log.error("创建失败", ex);
}
return userRepository.findByName(name).size();
}
@Transactional // 不生效!private 方法无法被代理
private void createUserPrivate(UserEntity entity) {
userRepository.save(entity);
if (entity.getName().contains("test"))
throw new RuntimeException("用户名不合法");
}
}结果:用户名不合法,但用户创建成功了。
原因:Spring 默认用动态代理实现 AOP,private 方法压根代理不了。
修复:改成 public 方法。
内部 this 调用
java
@Service
public class UserService {
public int createUser(String name) {
try {
this.createUserPublic(new UserEntity(name)); //this 不是代理对象
} catch (Exception ex) {
log.error("创建失败", ex);
}
return userRepository.findByName(name).size();
}
@Transactional // 不生效!
public void createUserPublic(UserEntity entity) {
userRepository.save(entity);
if (entity.getName().contains("test"))
throw new RuntimeException("用户名不合法");
}
}原因:this 代表对象自己,Spring 注入的是代理对象,this 调用跳过了代理。
修复:从 Controller 注入调用,或者在 Service 里注入自己:
java
@Service
public class UserService {
@Autowired
private UserService self; // 注入代理对象
public int createUser(String name) {
try {
self.createUserPublic(new UserEntity(name)); // 走代理
} catch (Exception ex) {
log.error("创建失败", ex);
}
return userRepository.findByName(name).size();
}
@Transactional
public void createUserPublic(UserEntity entity) {
userRepository.save(entity);
if (entity.getName().contains("test"))
throw new RuntimeException("用户名不合法");
}
}坑二:事务生效了但没回滚
自己 catch 了异常
java
@Transactional
public void createUser(String name) {
try {
userRepository.save(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("创建失败", ex); // 异常被吞了
}
}原因:Spring 用 try-catch 包裹事务方法,异常被 catch 了,没传播出去,Spring 不知道要回滚。
修复:手动标记回滚,或者重新抛出异常:
java
@Transactional
public void createUser(String name) {
try {
userRepository.save(new UserEntity(name));
throw new RuntimeException("error");
} catch (Exception ex) {
log.error("创建失败", ex);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 手动回滚
}
}受检异常不回滚
java
@Transactional
public void createUser(String name) throws IOException {
userRepository.save(new UserEntity(name));
Files.readAllLines(Paths.get("not-exist")); // IOException 是受检异常
}原因:Spring 默认只对 RuntimeException 和 Error 回滚。受检异常被认为是"业务预期内的返回",默认不回滚。
修复:声明要回滚的异常类型:
java
@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
public void createUser(String name) throws IOException {
userRepository.save(new UserEntity(name));
Files.readAllLines(Paths.get("not-exist"));
}坑三:子事务影响了主事务
场景:主用户要创建成功,子用户失败无所谓
java
@Service
public class SubUserService {
@Transactional
public void createSubUser(UserEntity entity) {
userRepository.save(entity);
throw new RuntimeException("子用户创建失败");
}
}
@Service
public class UserService {
@Autowired
private SubUserService subUserService;
@Transactional
public void createUser(UserEntity entity) {
createMainUser(entity); // 插入主用户
subUserService.createSubUser(entity); // 插入子用户(会失败)
}
private void createMainUser(UserEntity entity) {
userRepository.save(entity);
}
}结果:主用户也创建失败了。
原因:默认传播行为是 REQUIRED,在一个事务里,子方法失败会导致整个事务回滚。
修复:让子方法开启新事务
java
@Service
public class SubUserService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 开启新事务
public void createSubUser(UserEntity entity) {
userRepository.save(entity);
throw new RuntimeException("子用户创建失败");
}
}
@Service
public class UserService {
@Transactional
public void createUser(UserEntity entity) {
createMainUser(entity); // 主用户 - 成功
try {
subUserService.createSubUser(entity); // 子用户 - 失败,但独立事务
} catch (Exception ex) {
log.error("子用户失败了", ex); // 吞掉异常,不影响主事务
}
}
}原理:REQUIRES_NEW 会挂起当前事务,开启新事务。子事务失败只回滚自己,主事务不受影响。
一图总结
坑一:事务不生效
├── private 方法 → 改成 public
└── this 自调用 → 从外部调用,或注入自己
坑二:不回滚
├── 吞了异常 → 手动 setRollbackOnly 或重新抛出
└── 受检异常 → rollbackFor = Exception.class
坑三:子事务影响主事务
└── 用 REQUIRES_NEW 开启独立事务调试小技巧
开启事务 Debug 日志,看事务到底有没有生效:
yaml
logging:
level:
org.springframework.orm.jpa: DEBUG看日志里的事务名是不是你方法的名字,如果不是,说明没走到你的方法事务:
bash
# 生效:事务名是你的方法
Creating new transaction with name [com.xxx.UserService.createUser]
# 不生效:事务名是 JPA 的 save 方法
Creating new transaction with name [SimpleJpaRepository.save]总结
| 坑 | 原因 | 修复 |
|---|---|---|
| private 方法 | 代理不到 | 改成 public |
| this 调用 | 绕过代理 | 从外部调用 |
| 吞了异常 | 异常没传播 | setRollbackOnly |
| 受检异常 | 默认不回滚 | rollbackFor = Exception.class |
| 子事务影响主事务 | 同一个事务 | REQUIRES_NEW |
事务没处理好的 bug,平时不容易发现,一到高并发就数据乱了。所以写完代码多想一想,事务到底有没有生效。