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;
- 脏读(只在 RU):
- 实现与注意
- 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。
- AOP 代理(JDK/CGLIB)拦截
- 源码级实现细节(传播行为如何生效)
- 激活入口(配置):
@EnableTransactionManagement→TransactionManagementConfigurationSelector→ 注册ProxyTransactionManagementConfiguration- 定义
BeanFactoryTransactionAttributeSourceAdvisor(切点) - 使用
AnnotationTransactionAttributeSource解析@Transactional - 注入
TransactionInterceptor(拦截器)
- 定义
- 代理创建:基础设施自动代理器(如
InfrastructureAdvisorAutoProxyCreator)为匹配切点的方法创建 JDK/CGLIB 代理,解决横切逻辑织入 - 拦截主链:
TransactionInterceptor#invoke→TransactionAspectSupport#invokeWithinTransaction1) 解析事务属性:TransactionAttributeSource#getTransactionAttribute2) 解析事务管理器:determineTransactionManager(支持transactionManager指定) 3) 按传播语义开/加入事务:createTransactionIfNecessary→PlatformTransactionManager#getTransaction4) 调用业务方法:invocation.proceed()5) 正常则commit,异常走completeTransactionAfterThrowing判定回滚(TransactionAttribute#rollbackOn(Throwable),默认仅回滚RuntimeException/Error) - 传播决策核心:
AbstractPlatformTransactionManager#getTransaction(TransactionDefinition)- 若存在事务(
isExistingTransaction):PROPAGATION_REQUIRED/SUPPORTS/MANDATORY:加入当前事务(共享连接与同步)PROPAGATION_REQUIRES_NEW:suspend挂起当前事务 →doBegin开新事务 → 结束后resumePROPAGATION_NOT_SUPPORTED:suspend挂起,以非事务方式执行 →resumePROPAGATION_NEVER:存在事务直接抛IllegalTransactionStateExceptionPROPAGATION_NESTED:如支持保存点(useSavepointForNestedTransaction)则在当前事务createSavepoint,失败回滚到保存点;否则可能抛NestedTransactionNotSupportedException
- 若不存在事务:
REQUIRED/REQUIRES_NEW/NESTED:doBegin开启新事务(NESTED在无外部事务时等价于新事务)SUPPORTS:非事务执行MANDATORY:抛IllegalTransactionStateExceptionNOT_SUPPORTED/NEVER:非事务执行
- 若存在事务(
- 数据源事务实现(典型):
DataSourceTransactionManager- 开启:
doBegin获取连接并配置隔离级别、setReadOnly(true)、setAutoCommit(false),绑定ConnectionHolder至TransactionSynchronizationManager - 挂起/恢复:
doSuspend/doResume解绑/重新绑定资源 - 提交/回滚:
doCommit/doRollback;NESTED 通过SavepointManager创建/回滚保存点
- 开启:
- 资源与同步:
TransactionSynchronizationManager- 线程级别绑定资源:
bindResource/unbindResource(如DataSource → ConnectionHolder) - 暴露上下文:
isActualTransactionActive、getCurrentTransactionName、isCurrentTransactionReadOnly - 注册同步回调:
registerSynchronization(JPA/Hibernate flush、MQ 出库回调等借此挂接)
- 线程级别绑定资源:
- 隔离与只读映射:
TransactionDefinition→ JDBCConnection#setTransactionIsolation与setReadOnly(是否真正生效取决于驱动与数据库) - 关键类型速览:
TransactionAttribute/RuleBasedTransactionAttribute(回滚规则)、RollbackRuleAttribute、TransactionStatus(事务状态/保存点控制)
- 激活入口(配置):
JPA/Hibernate Flush 时机与事务边界
- Flush 不等于提交:Flush 将持久化上下文(一级缓存)中的变更同步到数据库,但仍处于当前数据库事务内,直到
commit。 - 触发时机(Hibernate FlushModeType.A 0UTO 默认):
- 查询前:为保证查询结果与当前持久化上下文一致,可能在执行查询前先 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;这只是“优化提示”,并不强制禁止写入。 - 仍需数据库权限控制与代码自律(如不在只读事务中执行写操作)。
- Spring 在使用 Hibernate 时,
- 一致性影响:由于 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) } }
- REQUIRED 与 REQUIRES_NEW:
- 常见陷阱
- 自调用不生效(同类内方法互调绕过代理);将被调方法提取到另一
@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/reserve、confirm、cancel;实现幂等、空回滚与悬挂处理。
七、测试与验证建议
- 隔离级别:集成测试开两连接/两线程,用
CountDownLatch控制交错,验证 RC/RR 行为与锁持有。 - 传播机制:SpringBootTest + Testcontainers,验证
REQUIRES_NEW独立提交、NESTED保存点回滚。 - 一致性链路:本地起 MQ(Kafka/RabbitMQ),模拟网络抖动、重复投递、乱序,核验幂等。
- 观测性:为每个事务/消息打 traceId、msgId,收集提交时延、重试次数、DLQ 速率等指标。
八、实施注意
- 严格控制事务边界与时长:仅包裹必要的 DB 写入与同库查询,不包远程调用/外部 IO。
- 明确每条链路的目标一致性等级与降级策略。
- 把“补偿与对账”作为一等公民:对账任务、自动巡检、纠偏脚本。