Java Spring 事务传播与隔离级别陷阱

事务机制在软件开发中扮演举足轻重的角色。本文系统介绍数据库与分布式事务的原理与应用、隔离级别与典型并发现象示例,并结合 Spring 的传播机制与实现原理,最后从 CAP 视角给出大型系统中确保“相对一致性”的工程方案与实战蓝本。

一、为何需要事务

  • 目标:在并发与故障条件下,保证数据正确性与可预期性。
  • ACID:原子性、一致性、隔离性、持久性;工程上需在性能、可用性与一致性间权衡。

二、数据库事务与隔离级别(含示例)

  • 常见隔离级别与现象
    • Read Uncommitted:可能脏读;几乎不用。
    • Read Committed (RC):避免脏读;仍有不可重复读与幻读(PostgreSQL 默认)。
    • Repeatable Read (RR):同一事务内多次读取结果一致;InnoDB 的 RR 基于 MVCC + Next-Key Lock,普通一致性读看到“快照”,更新扫描加间隙锁抑制幻行(MySQL 默认)。
    • Serializable:最强隔离;性能代价大,通常依赖锁或乐观并发控制。
  • 并发现象最小示例
    • 脏读(只在 RU):
      -- 会话A
      BEGIN;
      UPDATE account SET balance = balance - 100 WHERE id = 1; -- 未提交
      
      -- 会话B(RU)
      SELECT balance FROM account WHERE id = 1; -- 读到未提交数据(脏读)
      
    • 不可重复读(RC):
      -- 会话A
      BEGIN;
      SELECT balance FROM account WHERE id = 1; -- 读到 1000
      
      -- 会话B
      BEGIN; UPDATE account SET balance = 900 WHERE id = 1; COMMIT;
      
      -- 会话A
      SELECT balance FROM account WHERE id = 1; -- 再读到 900(不可重复读)
      COMMIT;
      
    • 幻读(RC 或部分 RR 场景):
      -- 会话A
      BEGIN;
      SELECT * FROM orders WHERE amount > 100; -- 返回 N 行
      
      -- 会话B
      INSERT INTO orders(id, amount) VALUES(999, 200); COMMIT;
      
      -- 会话A
      SELECT * FROM orders WHERE amount > 100; -- 返回 N+1 行(幻读)
      COMMIT;
      
  • 实现与注意
    • MVCC:RC/RR 通过快照读减少锁冲突;RR 在 InnoDB 下对“锁定读/更新”使用 Next-Key Lock 抑制幻读。
    • 加锁读SELECT ... FOR UPDATE/LOCK IN SHARE MODE 可确保当前读一致且参与加锁,抑制写冲突与幻读。
    • 建议:默认 RC/RR;强一致写路径(转账、库存)采用 RR + 加锁读,或使用可控的序列化/业务锁。

三、分布式事务模式与取舍

  • XA/2PC(协调器 + 参与者,两阶段提交)
    • 优点:强一致;缺点:阻塞、对资源管理器要求高、性能与可用性差;云原生场景较少采用。
  • TCC(Try-Confirm-Cancel)
    • 优点:业务可感知,接口粒度可控;缺点:实现复杂,需要补偿与悬挂/空回滚处理;适用于账务/库存等核心域。
  • Saga(编排/舞蹈)
    • 优点:最终一致、扩展性好;缺点:中间态可见、补偿设计复杂;适用于电商下单等长链路。
  • 可靠消息 + Outbox
    • 优点:本地事务落库,与“待发消息”同库同事务,异步发布,消费端幂等;工程落地成熟。
    • 缺点:引入异步与补偿复杂度;需要投递保证与去重。

四、Spring 事务:传播机制与实现原理

  • 实现原理
    • AOP 代理(JDK/CGLIB)拦截 @Transactional 方法,委派 TransactionInterceptor
    • PlatformTransactionManager(如 DataSourceTransactionManager/JpaTransactionManager)负责开启/提交/回滚。
    • TransactionSynchronizationManager 用 ThreadLocal 绑定连接/资源与同步回调。
    • 回滚规则:默认对 RuntimeException/Error 回滚;受检异常需显式 rollbackFor
  • 源码级实现细节(传播行为如何生效)
    • 激活入口(配置):@EnableTransactionManagementTransactionManagementConfigurationSelector → 注册 ProxyTransactionManagementConfiguration
      • 定义 BeanFactoryTransactionAttributeSourceAdvisor(切点)
      • 使用 AnnotationTransactionAttributeSource 解析 @Transactional
      • 注入 TransactionInterceptor(拦截器)
    • 代理创建:基础设施自动代理器(如 InfrastructureAdvisorAutoProxyCreator)为匹配切点的方法创建 JDK/CGLIB 代理,解决横切逻辑织入
    • 拦截主链:TransactionInterceptor#invokeTransactionAspectSupport#invokeWithinTransaction 1) 解析事务属性:TransactionAttributeSource#getTransactionAttribute 2) 解析事务管理器:determineTransactionManager(支持 transactionManager 指定) 3) 按传播语义开/加入事务:createTransactionIfNecessaryPlatformTransactionManager#getTransaction 4) 调用业务方法:invocation.proceed() 5) 正常则 commit,异常走 completeTransactionAfterThrowing 判定回滚(TransactionAttribute#rollbackOn(Throwable),默认仅回滚 RuntimeException/Error
    • 传播决策核心:AbstractPlatformTransactionManager#getTransaction(TransactionDefinition)
      • 若存在事务(isExistingTransaction):
        • PROPAGATION_REQUIRED/SUPPORTS/MANDATORY:加入当前事务(共享连接与同步)
        • PROPAGATION_REQUIRES_NEWsuspend 挂起当前事务 → doBegin 开新事务 → 结束后 resume
        • PROPAGATION_NOT_SUPPORTEDsuspend 挂起,以非事务方式执行 → resume
        • PROPAGATION_NEVER:存在事务直接抛 IllegalTransactionStateException
        • PROPAGATION_NESTED:如支持保存点(useSavepointForNestedTransaction)则在当前事务 createSavepoint,失败回滚到保存点;否则可能抛 NestedTransactionNotSupportedException
      • 若不存在事务:
        • REQUIRED/REQUIRES_NEW/NESTEDdoBegin 开启新事务(NESTED 在无外部事务时等价于新事务)
        • SUPPORTS:非事务执行
        • MANDATORY:抛 IllegalTransactionStateException
        • NOT_SUPPORTED/NEVER:非事务执行
    • 数据源事务实现(典型):DataSourceTransactionManager
      • 开启:doBegin 获取连接并配置隔离级别、setReadOnly(true)setAutoCommit(false),绑定 ConnectionHolderTransactionSynchronizationManager
      • 挂起/恢复:doSuspend/doResume 解绑/重新绑定资源
      • 提交/回滚:doCommit/doRollback;NESTED 通过 SavepointManager 创建/回滚保存点
    • 资源与同步:TransactionSynchronizationManager
      • 线程级别绑定资源:bindResource/unbindResource(如 DataSource → ConnectionHolder
      • 暴露上下文:isActualTransactionActivegetCurrentTransactionNameisCurrentTransactionReadOnly
      • 注册同步回调:registerSynchronization(JPA/Hibernate flush、MQ 出库回调等借此挂接)
    • 隔离与只读映射:TransactionDefinition → JDBC Connection#setTransactionIsolationsetReadOnly(是否真正生效取决于驱动与数据库)
    • 关键类型速览:TransactionAttribute/RuleBasedTransactionAttribute(回滚规则)、RollbackRuleAttributeTransactionStatus(事务状态/保存点控制)

JPA/Hibernate Flush 时机与事务边界

  • Flush 不等于提交:Flush 将持久化上下文(一级缓存)中的变更同步到数据库,但仍处于当前数据库事务内,直到 commit
  • 触发时机(Hibernate FlushModeType.A0UTO 默认):
    • 查询前:为保证查询结果与当前持久化上下文一致,可能在执行查询前先 flush(同表/相关实体时)。
    • 显式调用:EntityManager.flush()/Session.flush()
    • 事务完成前:在 commit 前自动 flush。
  • Flush 模式
    • AUTO(默认):必要时自动 flush(查询前/提交前)。
    • COMMIT:延迟到提交前再 flush(可能减少中途多次 flush)。
    • MANUAL:仅在显式调用 flush() 时触发。
  • 与 Spring 只读事务的关系
    • Spring 在使用 Hibernate 时,@Transactional(readOnly = true) 通常通过方言(如 HibernateJpaDialect)将会话 flush 模式降为 MANUAL,减少不必要的脏检查与 flush;这只是“优化提示”,并不强制禁止写入。
    • 仍需数据库权限控制与代码自律(如不在只读事务中执行写操作)。
  • 一致性影响:由于 AUTO 模式下查询可能触发 flush,很多约束/唯一键冲突会在“查询时”或“提交前”抛出,而非在调用 persist() 当下,测试时需注意断言位置。
@Transactional
public void demo(EntityManager em) {
  user.setEmail("dup@example.com");
  em.persist(user);
  // 这里未必立即抛异常
  em.createQuery("select u from User u where u.email = :e")
    .setParameter("e", "someone@example.com")
    .getResultList(); // 若查询触发 flush,唯一索引冲突可能在此处抛出
}

OpenEntityManagerInView 模式影响与建议

  • 原理OpenEntityManagerInViewFilter/Interceptor 在 Web 请求整个生命周期内绑定 EntityManager 到线程,使得 Controller/View 层在 Service 事务结束后仍可进行延迟加载,避免 LazyInitializationException
  • 风险
    • 模糊事务边界:在“视图渲染期”继续访问数据库,容易形成 N+1 查询与不可预期的长连接占用。
    • 可观测性降低:难以定位慢查询发生在业务层还是视图层。
    • 写路径混入:若无约束,视图层也可能触发写相关 flush(尽管少见,但应避免)。
  • Spring Boot 默认spring.jpa.open-in-view 在多数版本默认 true,官方已在 2.x/3.x 强烈提示谨慎使用。
  • 建议
    • 对写请求或核心域接口,设置 spring.jpa.open-in-view=false,在 Service 层完成 DTO 裁剪/投影映射与必要的 fetch join。
    • 对读多的页面,如确需开启,务必配合 @Transactional(readOnly = true)、限制查询数量、启用二级缓存/查询缓存谨慎优化。
    • 通过 EntityGraph/fetch join/投影(如 Spring Data JPA interface-based projections)解决懒加载需求。
# application.yml
spring:
  jpa:
    open-in-view: false
// 通过 fetch join 在事务内一次性加载所需数据
@Query("select o from Order o join fetch o.items where o.id = :id")
Optional<Order> findWithItems(@Param("id") Long id);

调用链流程图(简化)

sequenceDiagram
  autonumber
  participant Client
  participant Proxy as AOP Proxy
  participant TI as TransactionInterceptor
  participant TAS as TransactionAttributeSource
  participant TM as PlatformTransactionManager
  participant Biz as BusinessMethod

  Client->>Proxy: 调用 @Transactional 方法
  Proxy->>TI: invoke()
  TI->>TAS: 解析事务属性
  TI->>TM: getTransaction(def)
  TM-->>TI: TransactionStatus(可能挂起/新建/加入)
  TI->>Biz: proceed()
  Biz-->>TI: 返回或抛异常
  alt 正常返回
    TI->>TM: commit(status)
  else 异常
    TI->>TM: rollback(status)(按 rollbackOn 判定)
  end
  TI-->>Proxy: 返回结果
  Proxy-->>Client: 返回结果

传播决策流程(核心分支)

flowchart TD
  A[进入 getTransaction] --> B{是否存在事务}
  B -- 否 --> C{传播属性}
  C -- REQUIRED/REQUIRES_NEW/NESTED --> D[doBegin 新事务]
  C -- SUPPORTS/NOT_SUPPORTED/NEVER --> E[非事务执行]
  C -- MANDATORY --> F[抛 IllegalTransactionStateException]
  B -- 是 --> G{传播属性}
  G -- REQUIRED/SUPPORTS/MANDATORY --> H[加入当前事务]
  G -- REQUIRES_NEW --> I[suspend 挂起 -> doBegin 新事务]
  G -- NOT_SUPPORTED --> J[suspend 挂起 -> 非事务]
  G -- NEVER --> K[抛 IllegalTransactionStateException]
  G -- NESTED --> L[createSavepoint 保存点]

精简源码片段引用(Spring Framework)

// org.springframework.transaction.interceptor.TransactionInterceptor
public Object invoke(MethodInvocation invocation) throws Throwable {
    Class<?> targetClass = AopUtils.getTargetClass(invocation.getThis());
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
// org.springframework.transaction.interceptor.TransactionAspectSupport
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
                                         InvocationCallback invocation) throws Throwable {
    TransactionAttributeSource tas = getTransactionAttributeSource();
    TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    PlatformTransactionManager tm = determineTransactionManager(txAttr);
    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, methodIdentification(method, targetClass));
    try {
        Object ret = invocation.proceedWithInvocation();
        commitTransactionAfterReturning(txInfo);
        return ret;
    }
    catch (Throwable ex) {
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    }
    finally {
        cleanupTransactionInfo(txInfo);
    }
}
// org.springframework.transaction.support.AbstractPlatformTransactionManager
public final TransactionStatus getTransaction(TransactionDefinition definition)
        throws TransactionException {
    Object transaction = doGetTransaction();
    if (isExistingTransaction(transaction)) {
        // 根据传播行为: REQUIRED/SUPPORTS/MANDATORY/REQUIRES_NEW/NOT_SUPPORTED/NEVER/NESTED
        return handleExistingTransaction(definition, transaction, debugEnabled);
    }
    // 无事务,根据传播行为决定 doBegin 或非事务/异常
    return startTransaction(definition, transaction, debugEnabled);
}
// org.springframework.jdbc.datasource.DataSourceTransactionManager
protected void doBegin(Object transaction, TransactionDefinition definition) {
    Connection con = DataSourceUtils.getConnection(this.dataSource);
    con.setAutoCommit(false);
    prepareTransactionalConnection(con, definition); // 隔离级别/只读
    DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
    txObject.setConnectionHolder(new ConnectionHolder(con), true);
    TransactionSynchronizationManager.bindResource(this.dataSource, txObject.getConnectionHolder());
}

版本注记:上述调用链在 Spring Framework 5.3.x 与 6.x 之间主体一致;个别方法签名与内部重构可能略有差异,但关键职责与流程相同。

  • 传播行为(常用)
    • REQUIRED(默认):有则加入,无则新建;通用首选。
    • REQUIRES_NEW:挂起外部事务,新建新事务;常用于审计/日志/可靠消息,避免与外层同生共死。
    • SUPPORTS:有则加入,无则非事务。
    • MANDATORY:必须在事务内,否则异常。
    • NOT_SUPPORTED:挂起事务,以非事务方式执行;适合大查询/报表。
    • NEVER:存在事务则抛异常。
    • NESTED:保存点;内层失败可局部回滚(需 JDBC Savepoint 与 DataSourceTransactionManager 支持;JPA 不支持真嵌套)。
  • 关键示例
    • REQUIRED 与 REQUIRES_NEW:
      @Service
      public class OrderService {
        @Transactional // REQUIRED
        public void placeOrder() {
          inventoryService.deduct();
          auditService.record(); // 方法上标注 REQUIRES_NEW,独立提交
        }
      }
      
    • NESTED 局部回滚:
      @Transactional
      public void batchCreate(List<Item> items) {
        for (Item item : items) {
          userService.createOneNested(item); // @Transactional(propagation = NESTED)
        }
      }
      
  • 常见陷阱
    • 自调用不生效(同类内方法互调绕过代理);将被调方法提取到另一 @Service 或注入自身代理。
    • private/final 方法、构造器不拦截;异步/新线程无事务上下文。
    • 多数据源需独立 TransactionManager 或采用分布式事务模式。
    • readOnly=true 仅作优化提示,不保证不写;仍需权限与代码约束。
    • NESTED 仅在底层事务管理器支持保存点时才是真嵌套(如 DataSourceTransactionManager);JpaTransactionManager 不支持嵌套,可能抛异常或退化为新事务策略。

五、大型系统一致性策略(结合 CAP)

  • CAP 取舍:分布式系统必须容忍分区(P),在一致性(C)与可用性(A)间取舍。
    • CP 优先:撮合引擎、资金账本、强一致库存;采用单主/共识、严格限流与降级,牺牲可用性换正确性。
    • AP 优先:订单、推荐、搜索、报表;采用最终一致、补偿/重试、幂等、读写分离、缓存旁路。
  • 工程基线
    • 本地事务 + 出库表(Outbox)+ MQ
    • 幂等:幂等键/唯一索引/幂等表、去重缓存、乐观锁(version/timestamp)
    • 显式状态机:订单/库存/支付状态跃迁,防止“写两份不同真相”
    • 读模型:CQRS/物化视图;前台读可用性优先,后台对账纠偏
    • 重试与时序:至少一次投递 + 幂等消费,必要时按业务键分区保证顺序
    • 热点与锁:场景化选择悲观/乐观/分布式锁,控制锁粒度与超时

六、实战蓝本(落地建议)

  • 可靠消息 + Outbox 工作流 1) 业务服务本地事务内:写业务数据 + 写 outbox(状态 PENDING) 2) 提交后由发布器轮询/CDC 发布 MQ;成功标记 SENT 3) 消费者:用“消息ID 唯一索引/去重表”保证幂等;处理成功后标记完成 4) 失败自动重试(指数退避)+ 死信队列 + 人工干预

    @Transactional
    public void createOrder(CreateOrderCmd cmd) {
      orderRepo.save(order);
      outboxRepo.save(Outbox.of("OrderCreated", payload, msgId));
    }
    
  • Saga(编排型):编排器持久化 Saga 状态,顺序调用步骤;失败按反向补偿逐一回滚。
  • TCC 接口:资源服务提供 try/reserveconfirmcancel;实现幂等、空回滚与悬挂处理。

七、测试与验证建议

  • 隔离级别:集成测试开两连接/两线程,用 CountDownLatch 控制交错,验证 RC/RR 行为与锁持有。
  • 传播机制:SpringBootTest + Testcontainers,验证 REQUIRES_NEW 独立提交、NESTED 保存点回滚。
  • 一致性链路:本地起 MQ(Kafka/RabbitMQ),模拟网络抖动、重复投递、乱序,核验幂等。
  • 观测性:为每个事务/消息打 traceId、msgId,收集提交时延、重试次数、DLQ 速率等指标。

八、实施注意

  • 严格控制事务边界与时长:仅包裹必要的 DB 写入与同库查询,不包远程调用/外部 IO。
  • 明确每条链路的目标一致性等级与降级策略。
  • 把“补偿与对账”作为一等公民:对账任务、自动巡检、纠偏脚本。