广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Spring注解@Transactional是什么?具体的使用方法,核心要点一次讲明白,一文带你全面了解

Spring 里的 @Transactional 可以理解为一个声明式事务开关:加在方法或类上,Spring 容器在运行时通过代理对象自动管理数据库事务。跟编程式事务相比,它把 commit 和 rollback 的样板代码从业务方法中剥离出去,让代码更干净。但实际项目中,很多方法加了 @Transactional 却没有按预期回滚,问题往往出在对代理机制和属性配置的理解上。下面从底层机制、属性用法、失效场景和最佳实践四个方面展开说明。

Spring注解@Transactional是什么?具体的使用方法,核心要点一次讲明白,一文带你全面了解

一、@Transactional 的底层工作方式

Spring 的事务管理分为编程式事务和声明式事务两类。编程式事务需要手动获取事务管理器并调用 commit 或 rollback,代码繁琐但控制粒度细;声明式事务通过 AOP 代理把事务逻辑横切到目标方法前后,@Transactional 就是声明式事务的核心注解。它本身并不直接开启事务,而是告诉 Spring 容器:这个 Bean 的方法需要被事务代理包装。

具体流程是:Spring 在创建 Bean 时检测到类或方法上有 @Transactional,会为该 Bean 生成一个代理对象。外部调用方拿到的是代理对象,而不是原始对象。调用方法时,代理对象会先进入 TransactionInterceptor 拦截器,拦截器根据事务属性向 PlatformTransactionManager 申请开启事务,然后通过反射调用真实方法;方法正常返回则提交事务,抛出异常则根据回滚规则决定是否回滚。这也是为什么同类内部自调用会失效——因为 this 指向原始对象,绕过了代理。

事务管理器的选择取决于持久化技术。使用 JdbcTemplate 或 MyBatis 时通常对应 DataSourceTransactionManager;使用 Hibernate 或 JPA 时对应 JpaTransactionManager。Spring Boot 会根据 classpath 自动配置,一般无需手动注册。可以通过日志确认事务是否真正开启,例如在配置文件中设置 org.springframework.transaction 和 org.springframework.jdbc.datasource 的级别为 debug,日志里会出现 Creating new transaction 或 Rolling back transaction 等关键字。

logging:
  level:
    org.springframework.transaction: debug
    org.springframework.jdbc.datasource: debug

二、核心属性与配置方式

@Transactional 支持多个属性,最常用的是 propagation、isolation、rollbackFor、readOnly 和 timeout。默认情况下,传播行为是 REQUIRED,也就是当前存在事务就加入,不存在就新建。这个属性适合大多数本地事务场景,但如果一笔订单保存后需要记录审计日志,而日志不能因为订单回滚而丢失,就可以把审计方法设置为 REQUIRES_NEW,让它在独立事务中提交。

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeAuditLog(AuditLog log) {
    auditLogMapper.insert(log);
}

隔离级别一般使用数据库默认值即可,MySQL 默认为 REPEATABLE_READ。只有当业务对脏读、不可重复读、幻读有明确容忍度时,才需要显式调整。例如报表查询为了防止脏读可以设置 READ_COMMITTED。注意隔离级别最终受数据库支持能力限制,Spring 只是把值传递给数据库连接。

@Transactional(isolation = Isolation.READ_COMMITTED)
public Order readOrder(Long id) {
    return orderMapper.selectById(id);
}

回滚规则是踩坑最多的地方:Spring 默认只对 RuntimeException 和 Error 进行回滚,对 checked exception 不会回滚。假如你写了一个导入方法,方法签名抛出 IOException,内部导入到一半失败,Spring 不会自动回滚已写入的数据。这时必须通过 rollbackFor 指定受检异常也要回滚。

@Transactional(rollbackFor = Exception.class)
public void importOrders(List<Order> orders) throws IOException {
    for (Order order : orders) {
        orderMapper.insert(order);
    }
    fileService.parse();
}

只读事务通过 readOnly = true 开启,主要作用是给数据库优化提供提示,同时防止在只读方法中执行更新操作。事务超时通过 timeout 设置,单位是秒,超过时间后事务会被强制回滚。这两个属性在查询类方法和批量处理场景中都比较实用。

@Transactional(readOnly = true, timeout = 10)
public List<Order> queryOrders() {
    return orderMapper.selectList(null);
}

三、事务失效的常见场景与修复

第一种场景是同类方法自调用。示例中 processFromAnotherMethod 调用 this.createOrder,即便 createOrder 上有 @Transactional,调用也不会经过代理对象,因此事务不生效。修复方式有两种:一是把被调用方法拆到另一个 Service,二是注入自身代理对象后调用。

@Service
public class OrderService {
    @Autowired
    private OrderService self;
    public void processFromAnotherMethod(Order order) {
        self.createOrder(order);
    }
    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
        stockService.decrease(order.getProductId(), order.getCount());
    }
}

第二种场景是方法不是 public。Spring 默认通过 JDK 动态代理或 CGLIB 子类代理来拦截方法,只有 public 方法能被拦截。如果注解加在 private 方法上,Spring 不会报错,但事务完全不会开启。第三种是异常被业务代码吞掉。下面这段代码捕获了异常但没有重新抛出,事务管理器看不到异常,最终仍会提交。

@Transactional
public void updateUser(User user) {
    try {
        userMapper.update(user);
        accountService.deduct(user.getId(), 10);
    } catch (Exception e) {
        log.error("更新失败", e);
        // 事务仍然会提交,除非手动设置回滚标记
    }
}

修复方式是在 catch 中抛出 RuntimeException,或者调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 手动标记当前事务为只回滚。第四种场景是数据库存储引擎不支持事务,比如 MySQL 使用 MyISAM 表,事务不会产生实际回滚效果。业务表应使用 InnoDB 或其它支持事务的引擎。

第五种场景是多线程调用。事务资源绑定在当前线程上,如果在一个事务方法里通过线程池异步执行数据库操作,子线程不会自动加入当前事务。第六种是多数据源配置混乱。当项目里同时存在多个事务管理器时,必须通过 transactionManager 属性指定使用哪一个,否则 Spring 可能选择错误的事务管理器。第七种是类没有被 Spring 托管,比如自己 new 出来的对象或者忘记添加 Service 注解,自然也不会被代理。

四、调试技巧与落地建议

事务问题不要只靠猜,先打开 Spring 事务日志。日志中会明确打印方法名、事务名称、传播行为、隔离级别以及提交或回滚操作。根据日志可以快速判断代理是否生效,以及回滚是否因为异常类型不匹配而未触发。

在项目实践中,事务边界建议放在 Service 层的公共方法上,避免在 Controller 或 Mapper 方法上标注事务。一个事务方法不要执行过长时间,尤其不要在事务内调用远程 HTTP 接口或发送消息,否则会长时间占用数据库连接和行锁。查询类方法可以设置 readOnly = true,写入类方法控制好事务范围,不要把所有操作都揉在一个大事务里。

当业务逻辑非常复杂,声明式事务难以精确控制时,可以改用编程式事务模板 TransactionTemplate。它允许在方法内部显式决定提交、回滚和处理嵌套事务,代码可读性虽然略低,但控制粒度更高。

@Service
public class PaymentService {
    private final TransactionTemplate transactionTemplate;
    public PaymentService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }
    public void pay(Order order) {
        transactionTemplate.execute(status -> {
            orderMapper.updateStatus(order.getId(), "PAID");
            walletService.deduct(order.getUserId(), order.getAmount());
            return null;
        });
    }
}

掌握 @Transactional 的关键在于理解它依赖代理而不是魔法。只要把代理机制、属性取值和异常规则这三件事搞清楚,绝大多数事务问题都能在开发阶段发现并规避。

广告位 · 文章底部(后台-模块管理替换此区域)