深入解析 @Transactional——Spring 事务管理的核心

2,079 阅读5分钟

深入解析 @Transactional——Spring 事务管理的核心

@Transactional 是什么?

在开发 Spring Boot 项目的时候,我们往往会遇到这样的场景:一个方法里执行了多步数据库操作,但中间某一步出错了,导致数据出现了“半成功半失败”的情况。这样的数据不一致问题,可能会带来严重的业务风险。而 @Transactional 就是为了解决这个问题而生的。

它的核心作用就是 要么所有操作都成功,要么全部作废,保证数据库的完整性。但很多人用 @Transactional 时,总会遇到“事务没生效”、“回滚失败”、“操作部分成功”等问题,这背后其实有很多坑需要避开。今天我们就来深入拆解 @Transactional 的工作机制、常见陷阱以及最佳实践,帮你用好 Spring 的事务管理能力。

image.png

@Transactional 的基本用法

先来看一个最简单的示例,假设我们有一个删除部门的方法:

@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Transactional
    public void deleteDept(Long id) {
        deptMapper.delete(id);
        int error = 1 / 0; // 故意制造异常
    }
}

这个方法的逻辑很简单:删除部门后,我们故意制造了一个除零错误,看看事务是否会回滚。如果 @Transactional 正常生效,那么删除操作就不会被提交。但如果事务没生效,那就尴尬了,部门被删掉了,但异常依然抛出了。

Spring 事务默认的回滚规则

有时候,你可能会发现明明 @Transactional 加上了,但事务就是没回滚。Spring 默认的规则是:只有未捕获的 RuntimeException(运行时异常)或 Error 才会触发回滚,而普通的 Exception(检查异常)不会触发回滚

举个例子:

@Transactional
public void deleteDept(Long id) throws IOException {
    deptMapper.delete(id);
    throw new IOException("不会回滚!");
}

这里抛出了 IOException,但数据库的删除操作依然提交了。这是因为 IOException检查异常(Checked Exception),Spring 默认不会回滚。如果你希望无论什么异常都触发回滚,需要这样写:

@Transactional(rollbackFor = Exception.class)
public void deleteDept(Long id) throws IOException {
    deptMapper.delete(id);
    throw new IOException("现在会回滚了!");
}

所以,如果你希望所有异常都能回滚,最好加上 rollbackFor = Exception.class,避免出现“事务看起来生效了,但并没有真正回滚”的情况。

try-catch 导致事务失效

你可能会写这样的代码:

@Transactional
public void deleteDept(Long id) {
    try {
        deptMapper.delete(id);
        int x = 1 / 0; // 触发异常
    } catch (Exception e) {
        System.out.println("发生异常,但事务未回滚");
    }
}

看上去是个挺合理的异常处理,但问题来了:事务不会回滚!

为什么?因为异常被 catch 了,Spring 根本感知不到异常的发生,认为你的方法执行得好好的,于是就把事务提交了。

正确的做法是要么 让异常抛出去,要么 手动抛出一个新的 RuntimeException

@Transactional
public void deleteDept(Long id) {
    try {
        deptMapper.delete(id);
        int x = 1 / 0;
    } catch (Exception e) {
        throw new RuntimeException("手动抛出异常,确保事务回滚", e);
    }
}

或者直接这样写,让异常自然传播:

@Transactional
public void deleteDept(Long id) throws Exception {
    deptMapper.delete(id);
    int x = 1 / 0; // 事务会回滚
}

总结一下:Spring 只有在方法抛出异常时,才会触发回滚。如果你在 catch 里吞掉了异常,那事务也就不会回滚了

事务为什么有时候会失效?

除了 try-catch,还有一些常见情况会让 @Transactional 失效,看看你是否踩过这些坑。

1. 方法不是 public

@Transactional 只会作用于 public 方法,如果你加在 privateprotected 方法上,事务不会生效:

@Transactional
private void deleteDept(Long id) { } // 事务不会生效!

Spring 事务是通过 代理机制 实现的,而 JDK 动态代理只能代理 public 方法,所以其他访问级别的方法都不行。正确写法是:

@Transactional
public void deleteDept(Long id) { }

2. 同一类里,方法互相调用

看下面的代码:

@Service
public class DeptServiceImpl {
    @Autowired
    private DeptMapper deptMapper;

    @Transactional
    public void deleteDept(Long id) {
        this.deleteEmp(id); // 事务不会生效!
    }

    @Transactional
    public void deleteEmp(Long id) {
        empMapper.delByDeptId(id);
    }
}

这里 deleteDept 方法调用了 deleteEmp,但 deleteEmp 上的 @Transactional 不会生效!原因是:Spring 的事务是基于代理的,this.deleteEmp(id) 直接调用了本类的方法,没有经过 Spring 代理,所以事务不会生效。

正确的做法是 通过 Spring 管理的 Bean 调用

@Service
public class DeptServiceImpl {
    @Autowired
    private DeptServiceImpl self;

    @Transactional
    public void deleteDept(Long id) {
        self.deleteEmp(id); // 事务生效!
    }

    @Transactional
    public void deleteEmp(Long id) {
        empMapper.delByDeptId(id);
    }
}

或者使用 ApplicationContext 获取代理对象,再调用方法。

3. 数据库引擎不支持事务

如果你用的 MySQL 表引擎是 MyISAM,事务是不可能生效的,因为 MyISAM 根本不支持事务!要确保你的表是 InnoDB:

SHOW TABLE STATUS WHERE Name = 'dept';
ALTER TABLE dept ENGINE = InnoDB;

@Transactional 的最佳实践

  1. 确保事务方法是 public,否则事务不会生效。
  2. 避免同一类内部调用 @Transactional 方法,可以使用 self.xxx() 代理调用。
  3. 异常要让 Spring 感知到,不要 try-catch 后直接吞掉。
  4. 检查异常默认不会回滚,如果需要回滚,使用 rollbackFor = Exception.class
  5. 数据库表必须支持事务,MyISAM 不支持事务,建议用 InnoDB。

总结

Spring 事务管理的核心思想是 原子性,让数据库操作要么全成功,要么全失败。但如果用 @Transactional 时不小心踩了坑,可能会导致事务失效,影响数据一致性。希望这篇文章能帮你理解 @Transactional 的运行原理,让你的 Spring Boot 开发更加稳定可靠!