Skip to content

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,平时不容易发现,一到高并发就数据乱了。所以写完代码多想一想,事务到底有没有生效。