💫为什么一行 `@Transactional` 能救火,也能“放火”?——Spring Boot 事务管理全解析!

举报
bug菌 发表于 2025/09/16 11:17:31 2025/09/16
【摘要】 🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 前言先打个气:如果你在读到这篇的时候,正因为一条“幽灵般的脏数据”把你...

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言

先打个气:如果你在读到这篇的时候,正因为一条“幽灵般的脏数据”把你从周末烧烤局里拉回了工位——别怕,锅八成在事务边界和传播行为上。今天我们把 Spring Boot 事务管理 这盘大菜从“底层锅底”到“配料蘸料”都摆上桌,不仅讲个明白,还给你上手能跑的代码,顺便用几张可视化流程图把“坑点”钉在墙上,避免你下次再被同一个坑绊倒。😤🔥

🧭 你将收获什么?

  • 彻底搞懂 Spring 事务抽象PlatformTransactionManager、代理与 AOP 的那点事
  • 声明式 vs 编程式事务的正确打开方式,何时该用哪一种
  • @Transactional 每个参数的“性格与脾气”:propagationisolationrollbackFortimeoutreadOnly
  • 传播行为的“串场效应”:REQUIREDREQUIRES_NEWNESTED 等到底怎么选
  • 隔离级别的“四宗罪”:脏读、不可重复读、幻读、读写冲突如何被隔离
  • 回滚的“八卦”:为啥受检异常不回滚?怎么自定义?如何捕获后仍然回滚?
  • 多数据源、本地事务/JTA、测试中的事务、异步与事务、消息/Outbox/Saga 的实战建议

🗺️ 导航

  1. 🏗️ Spring Boot 事务管理概述
  2. 🎭 声明式事务 与 编程式事务
  3. 🏷️ @Transactional 注解的使用
  4. 🔁 事务传播行为 与 隔离级别
  5. 🧨 事务回滚机制(含常见踩坑与规避)
  6. 🧪 可运行的演示项目:H2 + JPA + Service 分层 + 集成测试
  7. 🧰 生产环境实战清单:多数据源、幂等与重试、异步与消息、监控与压测
  8. 🧴 收尾与复盘:一张表格搞定常见场景选型

备注:文中所有代码均可复制粘贴即跑(使用 H2 内存库演示),流程图用 Mermaid 展示关键路径,尽量把抽象问题“画出来”。🧑‍🎨

1) 🏗️ Spring Boot 事务管理概述

事务是数据库操作的原子单元,必须满足 ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。Spring 做的并不是“重造数据库事务”,而是提供了一个优雅的事务抽象层:统一接上不同的事务管理器(JDBC、JPA、JTA……),并通过 AOP 代理把“开启/提交/回滚”这类模板化逻辑从你的业务代码里“切”出来。

核心角色一眼看清:

  • PlatformTransactionManager:事务总管。常见实现:

    • DataSourceTransactionManager(JDBC/单机库的本地事务)
    • JpaTransactionManager(JPA/Hibernate)
    • JtaTransactionManager(分布式/XA,整合 Atomikos/Narayana 等)
  • @EnableTransactionManagement(Spring Boot Starter 已自动开启):让 @Transactional 生效

  • AOP 代理:基于代理包装你的 Bean 方法,在方法前后织入事务边界逻辑(注意自调用 self-invocation 的坑

来一张高层流程图感受一下“代理怎么织、事务怎么开”:

Client(调用者)Proxy(事务代理)Service(目标Bean)PlatformTransactionManagerDatabase调用 public method()begin() 根据注解/传播/隔离执行业务逻辑commit()COMMITrollback()ROLLBACKalt[业务正常][抛异常(匹配回滚规则)]返回结果/异常Client(调用者)Proxy(事务代理)Service(目标Bean)PlatformTransactionManagerDatabase

一句话总结:Spring 把你从琐碎的“开关事务”中解放出来,但你需要设计好事务边界,理解代理调用时机传播行为回滚规则

2) 🎭 声明式事务 与 编程式事务

2.1 声明式事务(主角)

  • @Transactional 标注类/方法即可
  • 优点:简洁、可读、统一;和切面能力天然契合
  • 使用面:80%+ 的业务

2.2 编程式事务(王牌备胎)

  • TransactionTemplatePlatformTransactionManager 手工编排
  • 适用:需要细粒度控制(同一方法里“分段提交/部分新事务/回滚但不中断主流程”等)、复杂异常/补偿跨模块不便暴露注解

编程式事务示例(精选常见诉求:主逻辑失败要回滚,日志必须单独新事务写入):

@Service
@RequiredArgsConstructor
public class OrderFacade {
    private final TransactionTemplate txTemplateMain;      // 默认 REQUIRED
    private final TransactionTemplate txTemplateRequiresNew;// 显式 REQUIRES_NEW
    private final AuditLogRepository auditRepo;
    private final OrderRepository orderRepo;

    public Long placeOrder(OrderDTO dto) {
        return txTemplateMain.execute(status -> {
            Long orderId = orderRepo.createOrder(dto); // 主流程
            try {
                txTemplateRequiresNew.execute(s2 -> {  // 单独新事务写审计日志
                    auditRepo.log("ORDER_PLACED", orderId);
                    return null;
                });
            } catch (Exception e) {
                // 审计失败不影响主流程,但要监控告警
                // log.warn("audit failed", e);
            }
            // 主流程可能继续更多操作...
            return orderId;
        });
    }
}

要点REQUIRES_NEW 让“审计日志”即使主事务失败也能单独提交,这在安全/合规/追踪场景很常见。

3) 🏷️ @Transactional 注解的使用

3.1 常用参数一图懂

参数 作用 常见取值/示例 细节/坑点
propagation 传播行为 REQUIRED(默认)、REQUIRES_NEWNESTED 决定与当前事务的关系
isolation 隔离级别 DEFAULTREAD_COMMITTEDREPEATABLE_READSERIALIZABLE 不同数据库默认值不同
rollbackFor 指定哪些异常回滚 rollbackFor=Exception.class 默认只对运行时异常回滚
noRollbackFor 指定哪些异常不回滚 noRollbackFor=BizWarn.class 精准控制
timeout 超时秒数 timeout=5 超时会触发回滚
readOnly 只读优化 readOnly=true 可能触发数据库/ORM 优化

3.2 基础用法与推荐姿势

  • 尽量标注在 Service 层(聚合用例/业务边界),避免 Controller 直接开事务
  • 类上标注 作为默认策略,在方法上细化覆盖
  • 方法必须是 public 才能被代理(JDK 动态代理时),同类内部自调用不会走代理(大坑,见下)
@Service
@Transactional(readOnly = true) // 默认只读
public class UserQueryService {
    private final UserRepository repo;
    public UserDTO findById(Long id) {
        return repo.findById(id).map(UserDTO::from).orElseThrow();
    }
}

@Service
public class UserCommandService {
    @Transactional // 写操作单独声明
    public Long createUser(CreateUserCmd cmd) { /* ... */ }

    @Transactional(rollbackFor = Exception.class, timeout = 3)
    public void changeEmail(Long uid, String email) { /* ... */ }
}

3.3 自调用(Self-Invocation)大坑!

@Service
public class PaymentService {
    @Transactional
    public void pay() { /* ... */ }

    public void orchestrate() {
        // ❌ 自调用 pay():不会经过代理,事务不起效
        this.pay();
    }
}

修复姿势

  • pay() 提取到另一个 Bean,通过注入调用
  • 或者注入“自己”的代理 PaymentService proxy = AopContext.currentProxy()(需开启 exposeProxy=true,但不太优雅)
  • 更推荐:按职责拆分 Service,让边界清晰也更易测

4) 🔁 事务传播行为 与 隔离级别

4.1 传播行为(Propagation)

一句“人格化”理解:“我遇到一个正在进行的事务,我要怎么办?”

传播行为 白话 典型用途
REQUIRED 有就加入,没有就新建 默认首选
REQUIRES_NEW 不管有没有,都新开一个 审计日志、发送消息、补偿动作
SUPPORTS 有就用,没有就算了 只读查询、缓存
MANDATORY 必须在事务中,否则报错 强制要求
NOT_SUPPORTED 挂起当前事务,以非事务方式执行 访问慢外部接口,避免长事务
NEVER 必须没有事务,否则报错 极特殊
NESTED 有就开保存点,无则新建 局部可回滚(JDBC/保存点支持)

传播行为交互图(最常见 REQUIRED ↔ REQUIRES_NEW):

Parse error on line 2: ...D A[外层 Service.A() @Transactional(RE ----------------------^ Expecting 'SEMI', 'NEWLINE', 'SPACE', 'EOF', 'GRAPH', 'DIR', 'subgraph', 'SQS', 'SQE', 'end', 'AMP', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'ALPHA', 'COLON', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'START_LINK', 'LINK', 'STYLE', 'LINKSTYLE', 'CLASSDEF', 'CLASS', 'CLICK', 'DOWN', 'UP', 'DEFAULT', 'NUM', 'COMMA', 'MINUS', 'BRKT', 'DOT', 'PCT', 'TAGSTART', 'PUNCTUATION', 'UNICODE_TEXT', 'PLUS', 'EQUALS', 'MULT', 'UNDERSCORE', got 'PS'

要点REQUIRES_NEW 真的会挂起外层事务,开启新连接/新事务,相互独立提交与回滚

4.2 隔离级别(Isolation)

一句“症状对应药方”:

隔离级别 能防的现象 成本
READ_UNCOMMITTED 不能防啥(允许脏读)
READ_COMMITTED 防脏读 中(PostgreSQL 默认
REPEATABLE_READ 再加防不可重复读(幻读在不同 DB 不同表现) 较高(MySQL InnoDB 默认
SERIALIZABLE 最强一致(近似串行) 最高,吞吐骤降

四宗罪速记

  • 脏读:读到了别人未提交的数据
  • 不可重复读:同一事务内多次读同一行,值不同
  • 幻读:同一查询条件,前后 行数 变了(新增/删除)
  • 串行化冲突:最高隔离下并发被强制排队/回滚

隔离级别选择建议

  • 读多写少:READ_COMMITTED 足矣
  • 金融/库存高一致:REPEATABLE_READ + 乐观/悲观锁
  • 极端一致性:考虑 SERIALIZABLE 但要压测

5) 🧨 事务回滚机制(与你“想象的不一样”的地方)

5.1 Spring 默认回滚规则

  • 默认只对 RuntimeExceptionError 回滚

  • 受检异常(Exception)默认不回滚

    • 如果你抛的是业务异常(受检),结果却提交了,那就是这个规则在“背刺”你

定制方式

@Transactional(rollbackFor = Exception.class)
public void doBiz() throws Exception {
    // 任何 Exception 都回滚
}

5.2 捕获异常后如何仍然回滚?

下述代码中你捕获了异常,“默认”就不会回滚——因为代理没看到异常抛出。

@Transactional
public void foo() {
    try {
        bar();
    } catch (BizException e) {
        // 你吞了异常,外面看不到异常 —— 默认不会回滚
    }
}

两种方式要么抛回,要么手动标记:

// 方案1:重新抛出(推荐,清晰)
catch (BizException e) {
    throw e;
}

// 方案2:标记回滚(不抛出也可回滚)
catch (BizException e) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

5.3 @TransactionalEventListener 与提交后动作

  • 你可能希望“提交成功后再发消息/清缓存”
  • @TransactionalEventListener(phase = AFTER_COMMIT) 保证只在成功提交后触发
@Component
public class OrderEvents {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onOrderCreated(OrderCreatedEvent evt) {
        // 只在事务提交后执行
        // publish to MQ / clear cache / notify downstream
    }
}

配合 Outbox 模式:把要发布的消息先写到本地表(和业务数据同事务),再由后台守护任务可靠投递到 MQ,实现“最终一致”。

6) 🧪 可运行 Demo:H2 + Spring Data JPA + 事务全套演示

目标:跑起来就能复现 REQUIREDREQUIRES_NEWrollbackForreadOnlytimeoutNESTED(若使用 DataSourceTransactionManager 且数据库/驱动支持保存点)等,帮助你在本地快速体感行为差异。

6.1 pom.xml

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.demo</groupId>
  <artifactId>tx-lab</artifactId>
  <version>1.0.0</version>
  <properties>
    <java.version>17</java.version>
    <spring-boot.version>3.3.2</spring-boot.version>
  </properties>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

6.2 application.yml

spring:
  datasource:
    url: jdbc:h2:mem:txlab;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
logging:
  level:
    org.hibernate.SQL: debug
    org.springframework.transaction: debug

6.3 实体与仓库

@Entity
@Table(name = "t_account")
@Data @NoArgsConstructor @AllArgsConstructor @Builder
public class Account {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String owner;
    private Long balance; // 分为单位
}
public interface AccountRepo extends JpaRepository<Account, Long> {
    Optional<Account> findByOwner(String owner);
}

6.4 Service:演示多种事务

@Service
@RequiredArgsConstructor
public class AccountService {
    private final AccountRepo repo;

    // 读操作,默认只读
    @Transactional(readOnly = true)
    public Long balanceOf(String owner) {
        return repo.findByOwner(owner).map(Account::getBalance).orElse(0L);
    }

    // 基础写操作 REQUIRED
    @Transactional
    public void deposit(String owner, long amount) {
        Account acc = repo.findByOwner(owner).orElseGet(() -> repo.save(Account.builder()
                .owner(owner).balance(0L).build()));
        acc.setBalance(acc.getBalance() + amount);
        repo.save(acc);
    }

    // 超时演示(单位秒),强行睡眠触发回滚
    @Transactional(timeout = 1)
    public void slowOperation() {
        repo.save(Account.builder().owner("slow").balance(1L).build());
        try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
    }

    // 受检异常默认不回滚 —— 用 rollbackFor 自定义
    @Transactional(rollbackFor = Exception.class)
    public void transferWithCheckedException(String from, String to, long amount) throws Exception {
        withdraw(from, amount);
        deposit(to, amount);
        throw new Exception("业务校验失败,需要回滚"); // 将回滚
    }

    @Transactional
    public void withdraw(String owner, long amount) {
        Account acc = repo.findByOwner(owner).orElseThrow();
        if (acc.getBalance() < amount) throw new IllegalStateException("余额不足");
        acc.setBalance(acc.getBalance() - amount);
        repo.save(acc);
    }
}

6.5 传播行为:REQUIRES_NEWREQUIRED 的对撞

@Service
@RequiredArgsConstructor
public class AuditService {
    private final AccountRepo repo;

    // 审计日志这里就简单写入一条记录到账户表做演示
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void writeAudit(String owner, String note) {
        repo.save(Account.builder().owner("audit-" + owner + "-" + note).balance(0L).build());
    }
}

@Service
@RequiredArgsConstructor
public class FacadeService {
    private final AccountService accountService;
    private final AuditService auditService;

    // 外层 REQUIRED,内层 REQUIRES_NEW:内层独立提交
    @Transactional
    public void businessWithAudit(String owner) {
        accountService.deposit(owner, 100);
        try {
            auditService.writeAudit(owner, "deposit-start");
            if (true) throw new RuntimeException("外层故障:让我们看看审计是否提交?");
        } finally {
            // finally 中别乱吞异常:要么抛出,要么 setRollbackOnly
        }
    }
}

运行后你会发现:外层回滚,但审计“audit-owner-deposit-start”那条仍然留在库里,因为它是 REQUIRES_NEW 的独立事务。✅

6.6 NESTED(保存点)演示(可选)

  • 需要 DataSourceTransactionManager + 数据库驱动支持保存点
  • JPA 默认 JpaTransactionManager 不直接暴露嵌套保存点,想用 NESTED 更稳的是走 JDBC 层或切换管理器
  • 演示思路:外层插入 A,内层插入 B;内层回滚(保存点),外层继续提交,最终只有 A

实务里 NESTED 少见,但在大事务里局部容错有用;否则更多会选 REQUIRES_NEW 做“旁路”提交。

6.7 集成测试(看得见回滚)

@SpringBootTest
@Transactional // 每条测试默认回滚,保证幂等
class TxLabTests {
    @Autowired AccountService accountService;
    @Autowired FacadeService facadeService;

    @Test
    void testRollbackForCheckedException() {
        accountService.deposit("A", 100);
        assertThrows(Exception.class, () ->
            accountService.transferWithCheckedException("A", "B", 50)
        );
        assertEquals(100L, accountService.balanceOf("A")); // 回滚成功
        assertEquals(0L, accountService.balanceOf("B"));
    }

    @Test
    void testRequiresNewAuditSurvivesOuterRollback() {
        assertThrows(RuntimeException.class, () ->
            facadeService.businessWithAudit("C")
        );
        // 外层回滚,C 的 100 不应入账
        assertEquals(0L, accountService.balanceOf("C"));
        // 但审计记录(写到账户表的那条)在另一个测试事务里不可见;这里仅做行为说明
    }
}

7) 🧰 生产实战清单(真的能救命的那种)

7.1 多数据源与指定事务管理器

  • 配置多个 PlatformTransactionManager,用 @Transactional(transactionManager = "tmX") 指明
  • 跨库一致性:谨慎上 JTA/XA(Narayana/Atomikos),性能与复杂度高;优先 拆分边界 + Outbox + 最终一致性
  • 极少数核心强一致再考虑 XA,并务必压测

7.2 幂等与重试

  • 读已提交下偶发冲突/死锁,@Retryable + 幂等写入
  • 幂等关键:业务主键(如订单号)代替“裸插入自增”
  • 乐观锁(@Version)+ 失败重试,或悲观锁(for update)在热点短冲突

7.3 异步与事务

  • @Async 新线程不继承调用方事务;需要的话自己开事务
  • 回调/监听要用 @TransactionalEventListener(AFTER_COMMIT),避免在回滚时误触发下游

7.4 缓存一致性

  • 写 DB 成功后再 失效缓存,不要先删缓存再写库(异常会留下“空洞”)
  • 读穿透策略 + 双写/延时清理,配合消息投递兜底

7.5 监控与压测

  • 打开 org.springframework.transaction debug 了解事务边界
  • 监控连接池活跃数/等待时长,避免长事务(手欠的 Thread.sleep、外部慢接口在事务里)
  • 压测不同隔离级别下吞吐与冲突,别想当然上 SERIALIZABLE

8) 🧪 一图速览:从请求到回滚/提交

REQUIRED
REQUIRES_NEW
NESTED
进入Service方法
是否存在事务?
按注解创建新事务
传播行为?
加入当前事务
挂起外层并新建事务
创建保存点
执行业务逻辑
异常匹配回滚规则?
回滚或回滚到保存点
提交事务或恢复外层
返回异常
返回结果

9) 🧩 常见坑位清单(对号入座别再踩)

  1. 自调用导致事务失效:同类里 this.method() 不经过代理

    • ✅ 拆 Bean,或通过接口注入代理
  2. 受检异常不回滚Exception 默认不回滚

    • ✅ 指定 rollbackFor=Exception.class,或抛 RuntimeException
  3. 捕获异常吞掉:外层看不见异常自然不回滚

    • ✅ 重新抛出,或 setRollbackOnly()
  4. 长事务:事务里打外部 HTTP、睡眠、读大表全表扫

    • ✅ 把慢操作移出事务、分段提交、批处理
  5. readOnly=true 当真只读:有的数据库会启用只读优化,但你写了也可能报错/静默失败

    • ✅ 读写分离,读方法单独只读事务
  6. 传播行为滥用:处处 REQUIRES_NEW 造成连接耗尽、顺序语义混乱

    • ✅ 明确边界,谨慎新事务
  7. 隔离级别想当然:以为 REPEATABLE_READ 防了所有幻读

    • ✅ 了解目标数据库实现差异(MySQL 与 PostgreSQL 的区别)
  8. 测试不隔离:测试互相污染

    • @Transactional 测试默认回滚,或 @DirtiesContext 控制上下文
  9. 多数据源事务忘记指定管理器:导致事务管理失效

    • @Transactional(transactionManager="xxx")

10) 🍱 结论型备忘(场景 → 推荐)

场景 推荐
典型单体/单库业务 Service 层 @TransactionalREQUIRED + 合理 rollbackFor
写库 + 审计/消息可靠写 主流程 REQUIRED + 审计/消息 REQUIRES_NEW 或 Outbox
高一致读写 REPEATABLE_READ(MySQL)+ 乐观/悲观锁 + 幂等重试
跨库强一致 谨慎 JTA/XA;优先领域拆分 + 最终一致
慢外部调用 NOT_SUPPORTED 或移出事务
复杂同方法细粒度控制 TransactionTemplate 编程式事务
测试 方法级 @Transactional 自动回滚,构造初始数据用 @Sql

🎁 附:一个“从 0 到 1”的 Mini 场景(吞异常也回滚 + 提交后发消息)

用例:创建订单 → 扣减余额 → 写审计(新事务)→ 订单事件提交后异步发布

@Service
@RequiredArgsConstructor
public class OrderService {
    private final AccountService accountService;
    private final OrderRepo orderRepo;
    private final AuditService auditService;
    private final ApplicationEventPublisher publisher;

    @Transactional(rollbackFor = Exception.class)
    public Long createOrder(String user, long amount) {
        Order order = orderRepo.save(new Order(null, user, amount, "CREATED"));
        try {
            accountService.withdraw(user, amount); // 余额不足抛异常,整单回滚
        } catch (Exception e) {
            // 捕获后仍需回滚
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            throw e;
        }
        // 审计走独立新事务 —— 即使外层后续异常也能保留轨迹
        auditService.writeAudit(user, "order-created-" + order.getId());
        // 订单创建事件:等事务提交后再发
        publisher.publishEvent(new OrderCreatedEvent(order.getId()));
        return order.getId();
    }
}

@Component
@RequiredArgsConstructor
class OrderEventHandler {
    private final MessageGateway mq;
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onCreated(OrderCreatedEvent evt) {
        mq.publish("order.created", Map.of("orderId", evt.orderId()));
    }
}

这套组合拳的效果

  • 业务异常 → 订单回滚,余额不变
  • 审计日志(REQUIRES_NEW仍然存在(有痕可查)
  • 只有在订单提交成功后,消息才会被发布(避免脏事件)

🧠 最后,灵魂三问(也是面试高频)

  1. 为什么我的 @Transactional 不生效?

    • 自调用?非 public?代理类型不对?方法签名被 final?切点没匹配到?
  2. REQUIRES_NEWNESTED 有什么区别?

    • 前者独立事务(挂起外层),后者保存点回滚(受外层约束)
  3. 怎么保证“写库 + 发消息”要么都成功要么都失败?

    • 本地事务 + Outbox + AFTER_COMMIT 监听 + 后台可靠投递

🧾 小结(拍桌版)

事务不是“有就开一个”的开关,而是一门边界学。 Spring 帮你把“如何开关”标准化了,但“何时开、开多大、怎么组合、失败怎么处理”依然是你的技术与经验。掌握 @Transactional 的每个开关(传播/隔离/回滚/只读/超时),认清代理调用与自调用的“戏法”,把审计/消息/异步安排到对的事务相位,你的系统就会从“偶发脏数据惊魂”进化为“稳定厚重可观测”。🧘‍♂️

📌 彩蛋:事务调试“三件套”

  1. logging.level.org.springframework.transaction=debug 看边界
  2. logging.level.org.hibernate.SQL=debug 看 SQL
  3. Mermaid 把你的业务调用链画出来,找自调用与传播“错配点”

祝你以后每一次“提交”都胸有成竹,每一次“回滚”都心里有数。🙌 如果想要把这篇扩成“事务专题电子书”(更细到行级锁、MVCC、不同数据库实现差异、XA vs Saga 全对拍),跟我说一声,我继续加料!

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

  最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

  同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。

✨️ Who am I?

我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。