Spring编程式事务详解

Spring 编程式事务(Programmatic Transaction)详细使用指南

这份文档讲的是“不用 @Transactional,而是你在代码里手动开事务/提交/回滚”的玩法。适合:需要更细粒度控制、需要在循环/批处理里分段提交、需要按条件决定是否回滚、需要跨多个调用点拼装事务边界等场景。


1. 先把结论说清楚:什么时候用编程式事务

更适合用编程式事务的场景

  • 一个方法里有多段业务:某些段失败要回滚,某些段失败不回滚(或只回滚部分)。
  • 循环批处理:比如 1 万条数据,想每 500 条提交一次,失败只回滚当前批次。
  • 按条件决定提交/回滚:比如校验不通过就回滚;或某些异常允许提交。
  • 需要显式指定事务属性:传播行为、隔离级别、超时时间、只读等在运行时动态决定。
  • 多数据源/多事务管理器:你要手动选用哪个 PlatformTransactionManager

不建议用编程式事务的场景

  • 业务简单、只要“进方法 -> 开事务 -> 结束提交/异常回滚”:直接 @Transactional 更省心。
  • 你不想承担“忘记回滚/提交”的心智负担:声明式事务更不容易出错。

2. Spring 事务模型速览(理解几个核心对象)

Spring 编程式事务围绕三个核心东西:

  • PlatformTransactionManager:事务管理器(真正干活的,开启/提交/回滚)。
    • JDBC/MyBatis 常用:DataSourceTransactionManager
    • JPA 常用:JpaTransactionManager
  • TransactionDefinition:事务定义(传播行为、隔离级别、超时、只读等配置)。
    • 常用实现:DefaultTransactionDefinition
  • TransactionStatus:事务状态(当前事务是否新建、是否回滚标记、savepoint 等)。

编程式事务主要有两种写法:

  1. TransactionTemplate(推荐):更安全、更短、更不容易漏提交/回滚
  2. PlatformTransactionManager 手动 getTransaction/commit/rollback:更底层、更灵活(但更容易写错)

3. 写法一:TransactionTemplate(推荐)

3.1 基本用法(最常见)

@Service
public class OrderService {

    private final TransactionTemplate transactionTemplate;
    private final OrderMapper orderMapper;
    private final StockMapper stockMapper;

    public OrderService(PlatformTransactionManager txManager,
                        OrderMapper orderMapper,
                        StockMapper stockMapper) {
        this.transactionTemplate = new TransactionTemplate(txManager);
        this.orderMapper = orderMapper;
        this.stockMapper = stockMapper;
    }

    public Long createOrder() {
        return transactionTemplate.execute(status -> {
            // 1) 写订单
            orderMapper.insert(...);

            // 2) 扣库存
            stockMapper.decrease(...);

            // 3) 返回结果(会在 execute 结束时自动 commit)
            return 123L;
        });
    }
}
  • execute(...) 内部抛出运行时异常:默认会回滚。
  • execute(...) 正常返回:默认提交。

3.2 捕获异常但仍然回滚(容易踩坑点)

很多人会写成这样:

transactionTemplate.execute(status -> {
    try {
        doBiz();
    } catch (Exception e) {
        // 你 catch 了异常,Spring 看不到异常了 -> 会提交!
    }
    return null;
});

正确写法:catch 了异常也要显式标记回滚

transactionTemplate.execute(status -> {
    try {
        doBiz();
    } catch (Exception e) {
        status.setRollbackOnly(); // 显式回滚标记
        // 你也可以记录日志/转换异常
    }
    return null;
});

或者更直接:catch 后重新抛出运行时异常,让 Spring 自动回滚。

3.3 动态设置事务属性(传播/隔离/超时/只读)

TransactionTemplate tt = new TransactionTemplate(txManager);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
tt.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
tt.setTimeout(5);       // 秒
tt.setReadOnly(false);

tt.execute(status -> {
    // ...
    return null;
});

注意:readOnly=true 并不是“禁止写”,更多是提示数据库/驱动做优化;是否强制看数据库实现。

3.4 无返回值写法(TransactionCallbackWithoutResult)

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus status) {
        // ...
    }
});

4. 写法二:直接用 PlatformTransactionManager(更底层)

4.1 标准模板

public void doWorkManually() {
    DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

    TransactionStatus status = txManager.getTransaction(def);
    try {
        // 业务逻辑
        doBiz();

        txManager.commit(status);
    } catch (Exception ex) {
        txManager.rollback(status);
        throw ex;
    }
}

这种写法的特点:你完全掌控 commit/rollback,但也意味着:

  • 你必须保证所有路径都能正确回滚/提交
  • 代码更长,重复更多

4.2 需要 Savepoint(局部回滚)

Spring TransactionStatus 支持 savepoint(底层要支持,比如 JDBC)。

TransactionStatus status = txManager.getTransaction(def);
Object sp = null;
try {
    step1();

    sp = status.createSavepoint(); // 创建保存点
    step2();                       // 这里可能失败

    step3();
    txManager.commit(status);
} catch (Exception e) {
    if (sp != null) {
        status.rollbackToSavepoint(sp); // 仅回滚到保存点
        status.releaseSavepoint(sp);
        // 你也可以继续执行后续逻辑
        txManager.commit(status);
    } else {
        txManager.rollback(status);
    }
}

注意:Savepoint 不是“嵌套事务”的万能替代,它只是当前事务里的局部回滚点。


5. 事务传播行为(Propagation)怎么选(非常关键)

常用传播行为:

  • REQUIRED(默认):有事务就加入,没有就新建。适合大多数业务。
  • REQUIRES_NEW:不管外面有没有事务,都新开一个(外部事务挂起)。适合:记录日志/写审计/发通知等“我不想被外部回滚影响”的操作。
  • NESTED:嵌套事务(基于 savepoint)。外部事务存在时创建 savepoint;外部没事务就等同 REQUIRED。适合:希望“外部整体回滚”,但内部可以局部回滚。

一个典型例子:批处理分段提交

需求:循环处理 1000 条,每条失败不影响其他条。

for (Long id : ids) {
    transactionTemplate.execute(status -> {
        try {
            handleOne(id);
        } catch (Exception e) {
            status.setRollbackOnly(); // 回滚当前条
        }
        return null;
    });
}

如果你希望“外部还有大事务,但每条独立提交”,就用 REQUIRES_NEW

TransactionTemplate tt = new TransactionTemplate(txManager);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

for (Long id : ids) {
    tt.execute(status -> {
        handleOne(id);
        return null;
    });
}

6. 隔离级别(Isolation)怎么理解(别乱调)

Spring 的隔离级别基本就是数据库隔离级别的映射:

  • READ_UNCOMMITTED:可能脏读(一般别用)
  • READ_COMMITTED:避免脏读(很多库默认)
  • REPEATABLE_READ:避免不可重复读(MySQL InnoDB 常见默认)
  • SERIALIZABLE:最严格,性能最差

要点:

  • 隔离级别是数据库层能力,Spring 只是把参数传下去。
  • 如果你的数据源/数据库不支持你设置的隔离级别,可能会被忽略或抛异常(取决于驱动实现)。

7. 回滚规则(Rollback Rules)在编程式事务里怎么做

声明式事务里你会写:@Transactional(rollbackFor = Exception.class)
编程式事务里,核心逻辑是:

  • 抛运行时异常(RuntimeException/Error):默认回滚
  • 捕获异常:默认不会回滚(因为异常被吞了)
  • 想回滚但不想抛异常status.setRollbackOnly()

示例:遇到业务异常也要回滚,但你想返回错误码而不是抛异常:

Result r = transactionTemplate.execute(status -> {
    try {
        doBiz();
        return Result.ok();
    } catch (BizException e) {
        status.setRollbackOnly();
        return Result.fail(e.getCode(), e.getMessage());
    }
});

8. Spring Boot + MyBatis 下的典型配置要点

如果你是 Spring Boot + MyBatis(常见组合):

  • 默认会自动装配 DataSource
  • 事务管理通常是 DataSourceTransactionManager
  • 你只要注入 PlatformTransactionManagerTransactionTemplate 即可

8.1 直接注入 TransactionTemplate

@Service
public class PayService {

    @Resource
    private TransactionTemplate transactionTemplate;

    public void pay() {
        transactionTemplate.execute(status -> {
            // MyBatis Mapper 调用在同一事务里
            // ...
            return null;
        });
    }
}

8.2 如果你有多数据源(多个事务管理器)

你需要指定你要用哪个事务管理器:

@Bean
public TransactionTemplate orderTxTemplate(@Qualifier("orderTxManager") PlatformTransactionManager tm) {
    return new TransactionTemplate(tm);
}

然后在业务里注入 @Qualifier("orderTxTemplate") 使用。

多数据源“跨库一致性”别指望一个本地事务解决;要么上分布式事务/最终一致性,要么做业务补偿。


9. 编程式事务的常见坑(踩一次就长记性)

  1. catch 了异常没回滚

    • 解决:要么抛运行时异常,要么 status.setRollbackOnly()
  2. 事务没生效(尤其是你以为“调用了 @Transactional 就行”)

    • 编程式事务通常不会遇到“自调用失效”问题,因为它不是靠代理拦截
    • 但如果你混用声明式事务,要注意 AOP 代理规则
  3. REQUIRES_NEW 被外部连接池/线程模型影响

    • 它会挂起外部事务并新开事务,底层需要再拿一个连接
    • 连接池太小可能导致等待甚至死锁式卡住
  4. 在事务里做慢 IO(HTTP 调用、发 MQ、写大文件)

    • 事务时间越长,锁持有越久,性能越差
    • 通常做法:事务里只做 DB 关键写;IO 用 outbox/事件/消息异步做
  5. 只读事务里写数据

    • 不同数据库表现不一致;别赌实现,读写分开最省心

10. 和 @Transactional 的对比(你怎么选)

维度@Transactional(声明式)编程式事务
代码量
事务边界可读性强(注解就是边界)取决于你写的位置
动态控制(运行时决定传播/隔离等)不方便很方便
分段提交/循环批处理麻烦天生适合
出错概率相对低相对高(忘记回滚等)

经验法则:默认用 @Transactional,遇到复杂边界/批处理/动态策略再上编程式事务(优先 TransactionTemplate)。


11. 一个“真实感”更强的例子:下单 + 库存 + 失败降级

需求:

  • 写订单、扣库存必须同一个事务
  • 发送“站内信/通知”失败不影响下单成功(独立事务)
@Service
public class TradeService {

    private final TransactionTemplate mainTx;
    private final TransactionTemplate newTx; // REQUIRES_NEW
    private final OrderMapper orderMapper;
    private final StockMapper stockMapper;
    private final NoticeMapper noticeMapper;

    public TradeService(PlatformTransactionManager tm,
                        OrderMapper orderMapper,
                        StockMapper stockMapper,
                        NoticeMapper noticeMapper) {
        this.mainTx = new TransactionTemplate(tm);

        this.newTx = new TransactionTemplate(tm);
        this.newTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

        this.orderMapper = orderMapper;
        this.stockMapper = stockMapper;
        this.noticeMapper = noticeMapper;
    }

    public void placeOrder(Long userId, Long skuId) {
        mainTx.execute(status -> {
            orderMapper.insertOrder(userId, skuId);
            stockMapper.decrease(skuId);

            // 通知:失败也不影响主事务
            try {
                newTx.execute(s2 -> {
                    noticeMapper.insertNotice(userId, "下单成功");
                    return null;
                });
            } catch (Exception ignore) {
                // 记录日志即可,不影响主流程
            }

            return null;
        });
    }
}

12. 排查事务问题的实用手段

  • 打开 Spring 事务日志:
    • org.springframework.transaction / org.springframework.jdbc.datasource 调到 DEBUG
  • 打印当前是否在事务中:
    • TransactionSynchronizationManager.isActualTransactionActive()
  • MyBatis 场景关注:同一事务内是否复用同一个 Connection(日志可看)

13. 速记小抄

  • 推荐:TransactionTemplate.execute(...)
  • catch 了异常要回滚:status.setRollbackOnly()
  • 想让一段逻辑不受外部事务影响:PROPAGATION_REQUIRES_NEW
  • 想在大事务里局部回滚:PROPAGATION_NESTED(底层 savepoint 支持很关键)
  • 事务里别做慢 IO

14. 参考类/接口(你看源码时会遇到)

  • PlatformTransactionManager
  • AbstractPlatformTransactionManager
  • DataSourceTransactionManager
  • TransactionTemplate
  • DefaultTransactionDefinition
  • TransactionDefinition
  • TransactionStatus
  • TransactionSynchronizationManager

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值