ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring JPA事务异常:No EntityManager with actual transaction的根因与修复

Spring JPA事务异常:No EntityManager with actual transaction的根因与修复 1. 这个异常到底在说什么javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread—— 这个报错在 JPA 技术栈里出现的频率极高尤其是用 Spring 全家桶做持久层的时候。我第一次遇到它是在一个订单状态更新的接口里代码逻辑看起来完全没问题entityManager.merge()调用也写得好好的但一跑就抛这个异常堆栈信息指向EntityManager的某个方法提示当前线程没有可用的事务。先把这句话拆开看。TransactionRequiredException是 JPA 规范定义的一个运行时异常继承自PersistenceException。它的语义非常明确你调用的这个操作必须在一个活跃的事务里执行但当前线程上下文中找不到任何事务。后半句No EntityManager with actual transaction available for current thread是 Hibernate 作为 JPA 实现时给出的具体描述意思是它拿到的EntityManager是一个“共享代理”这个代理背后没有绑定真正的事务性会话。这里有一个关键概念需要先理清楚在 Spring 管理的环境中你通过PersistenceContext注入的EntityManager并不是一个真正的EntityManager实例而是一个线程绑定的代理对象。这个代理会在每次方法调用时去当前线程的上下文里查找有没有绑定一个“实际的”EntityManager。如果当前线程正处于一个 Spring 管理的事务中代理就能找到那个真正的、和事务关联的EntityManager调用正常转发。如果当前线程没有事务代理就找不到目标于是抛出这个异常。所以这个异常的本质不是“EntityManager 不存在”而是“当前线程没有事务导致 EntityManager 代理无法找到真正的执行者”。理解这一点后面的排查方向就清晰了。哪些操作会触发这个异常根据 JPA 规范以下几类操作强制要求事务上下文操作类型具体方法是否必须事务数据修改persist、merge、remove必须刷新同步flush必须查询createQuery执行executeUpdate必须锁定lock、refresh必须只读查询find、createQuery的getResultList不必须注意最后一行纯查询操作在没有事务的情况下是可以执行的这也是为什么很多人觉得“我查询没问题啊怎么一到保存就报错”。因为查询走的是不同的代码路径不强制要求事务。还有一个容易混淆的点Transactional注解加了为什么还是报这个错这就涉及到 Spring AOP 代理的生效条件、事务传播行为、以及自调用等一系列问题后面会逐一展开。2. 为什么事务没有生效六种典型场景拆解2.1 自调用导致代理失效这是最常见的原因没有之一。Spring 的Transactional是基于 AOP 代理实现的只有通过代理对象调用方法时事务拦截器才会生效。如果你在一个 Service 类的方法 A 里直接调用同一个类的另一个方法 B而 B 上标注了Transactional这个调用走的是this引用不经过代理事务注解形同虚设。Service public class OrderService { Autowired private OrderRepository orderRepository; public void updateOrderStatus(Long orderId) { // 这里直接 this 调用事务不生效 doUpdate(orderId); } Transactional public void doUpdate(Long orderId) { Order order orderRepository.findById(orderId).orElseThrow(); order.setStatus(PAID); // 如果没有事务这里就会抛 TransactionRequiredException orderRepository.save(order); } }这个坑我踩过不止一次。表面上看doUpdate上有Transactional但因为是从updateOrderStatus内部调用的Spring 的事务拦截器根本没有介入。解决办法有三种把doUpdate拆到另一个 Bean 里通过AopContext.currentProxy()获取代理对象再调用或者用TransactionTemplate手动管理事务。第一种最干净第三种最灵活。2.2 事务传播行为配置不当Transactional的propagation属性默认是REQUIRED意思是如果当前没有事务就新建一个如果有就加入。但如果你显式配置了SUPPORTS、NOT_SUPPORTED或NEVER情况就不一样了。SUPPORTS表示“有事务就用没有就算了”听起来很温和但如果你在这个方法里做了写操作没有事务时就会直接抛异常。NOT_SUPPORTED更狠它会挂起当前事务以非事务方式执行写操作必炸。NEVER则是明确禁止事务有事务反而报错。// 这种配置下如果外层没有事务写操作就会失败 Transactional(propagation Propagation.SUPPORTS) public void saveData(Data data) { entityManager.persist(data); // 抛 TransactionRequiredException }排查的时候先看Transactional的propagation值是不是被改过。很多团队在代码规范里会统一要求某些方法用特定传播行为但写操作的方法被误配成SUPPORTS的情况并不少见。2.3 异常被吞掉导致事务回滚标记丢失Spring 的事务管理依赖异常传播来判断是否需要回滚。默认情况下只有RuntimeException和Error会触发回滚受检异常不会。但更隐蔽的问题是如果异常在事务方法内部被try-catch吞掉了事务拦截器感知不到异常就不会标记回滚事务可能已经因为其他原因被标记为 rollback-only后续操作就会出问题。Transactional public void process() { try { // 某个操作触发了约束冲突 entityManager.persist(entity); } catch (Exception e) { // 异常被吞了但事务已经被标记为 rollback-only log.error(保存失败, e); } // 这里再操作就会出问题 entityManager.merge(otherEntity); }这种情况下异常信息可能不是直接的TransactionRequiredException而是UnexpectedRollbackException或者事务状态不一致导致的其他异常。排查时要看日志里有没有“Transaction silently rolled back”之类的提示。2.4 多数据源环境下事务管理器不匹配项目里配了多个数据源每个数据源有自己的DataSourceTransactionManager或JpaTransactionManager。如果你在操作数据源 A 的方法上事务管理器却绑定的是数据源 B那数据源 A 的EntityManager就找不到事务。Configuration public class DataSourceConfig { Bean Primary public JpaTransactionManager primaryTransactionManager( Qualifier(primaryEntityManagerFactory) EntityManagerFactory emf) { return new JpaTransactionManager(emf); } Bean public JpaTransactionManager secondaryTransactionManager( Qualifier(secondaryEntityManagerFactory) EntityManagerFactory emf) { return new JpaTransactionManager(emf); } }用Transactional的时候如果不指定transactionManager默认用的是Primary那个。操作非主数据源时必须显式指定Transactional(transactionManager secondaryTransactionManager) public void saveToSecondary(Data data) { // 操作从数据源 }这个坑在多数据源项目里非常典型尤其是项目初期只有一个数据源后来加了第二个老代码没改新代码又忘了指定就会出现“有的方法正常有的方法报错”的诡异现象。2.5 异步线程中丢失事务上下文Async注解的方法会在一个新线程里执行而 Spring 的事务上下文是绑定在ThreadLocal上的不会自动传播到新线程。所以如果你在一个事务方法里调用了Async方法异步方法里拿到的EntityManager代理是找不到事务的。Transactional public void batchProcess(ListData dataList) { for (Data data : dataList) { asyncSave(data); // 异步方法新线程没有事务 } } Async public void asyncSave(Data data) { entityManager.persist(data); // 抛 TransactionRequiredException }解决办法是在异步方法上也加Transactional让它在自己的线程里开启独立事务。但要注意这样每个异步操作就是一个独立事务失去了外层事务的原子性。如果业务上要求整体回滚就不能用异步或者需要引入分布式事务方案。2.6 测试环境缺少事务配置单元测试或集成测试里如果没有加Transactional或者Rollback测试方法本身不在事务中执行。如果测试代码直接调用EntityManager的写操作就会报这个错。SpringBootTest // 缺少 Transactional测试方法不在事务中 class OrderServiceTest { Autowired private OrderService orderService; Test void testSave() { // 如果 orderService 内部没有事务这里就会失败 orderService.saveOrder(new Order()); } }测试类上加Transactional可以让每个测试方法在事务中执行测试结束后自动回滚不污染数据库。这是 Spring 测试框架的标准做法但新手容易忘。3. 从源码层面理解事务代理的工作机制3.1 EntityManager 代理的创建过程当 Spring 容器启动时PersistenceAnnotationBeanPostProcessor会扫描所有标注了PersistenceContext的字段为它们注入一个SharedEntityManagerCreator创建的代理对象。这个代理实现了EntityManager接口但所有方法调用都会被拦截。拦截逻辑的核心在SharedEntityManagerCreator的内部类SharedEntityManagerInvocationHandler里。它的invoke方法大致做这么几件事获取当前线程绑定的EntityManagerFactory通过EntityManagerFactoryUtils尝试从TransactionSynchronizationManager获取当前线程绑定的EntityManagerHolder如果找到了就用那个真正的EntityManager执行目标方法如果没找到检查方法是否属于“必须事务”的类别如果是必须事务的方法抛出TransactionRequiredException关键在第 2 步和第 4 步。TransactionSynchronizationManager是 Spring 事务同步的核心类它内部维护了一组ThreadLocal其中resources这个ThreadLocal存储了当前线程绑定的资源包括EntityManagerHolder。当事务开启时JpaTransactionManager会调用TransactionSynchronizationManager.bindResource()把EntityManagerHolder绑定到当前线程。事务结束时再解绑。所以整个链路是Transactional→JpaTransactionManager开启事务 → 创建EntityManager→ 绑定到ThreadLocal→EntityManager代理从ThreadLocal找到真正的EntityManager→ 执行操作。任何一个环节断了都会导致代理找不到目标抛出这个异常。3.2 事务同步管理器的工作细节TransactionSynchronizationManager里和这个异常直接相关的方法有两个getResource(EntityManagerFactory)从当前线程的ThreadLocal里获取绑定的EntityManagerHolderisActualTransactionActive()判断当前线程是否有活跃的事务SharedEntityManagerInvocationHandler在找不到EntityManagerHolder时会进一步检查isActualTransactionActive()。如果返回false说明当前线程根本没有事务直接抛异常。如果返回true但找不到EntityManagerHolder说明事务存在但EntityManager没有正确绑定这通常是配置问题。有一个细节值得注意isActualTransactionActive()返回true但getResource()返回null的情况在JpaTransactionManager和DataSourceTransactionManager混用时会出现。比如你用DataSourceTransactionManager管理事务但用EntityManager做持久化事务管理器不会创建和绑定EntityManagerHolder代理就找不到目标。3.3 只读事务的特殊处理Transactional(readOnly true)是一个常被忽视的配置。它不仅仅是给数据库驱动一个提示在 JPA 层面也有实际影响。当readOnly true时JpaTransactionManager会设置EntityManager的FlushMode为MANUAL意味着 Hibernate 不会自动 flush。如果你在这个事务里做了写操作虽然不会立即抛TransactionRequiredException但数据不会同步到数据库直到事务结束也不会 flush造成“保存了但没生效”的假象。更严重的是某些 JPA 实现会在只读事务里对写操作直接抛异常。所以如果你看到TransactionRequiredException出现在一个标注了Transactional(readOnly true)的方法里先检查是不是误把写操作放进了只读事务。4. 系统化排查流程与实战修复方案4.1 排查决策树遇到这个异常不要急着改代码先按下面的顺序排查能省很多时间第一步确认异常抛出的具体位置。看堆栈信息里最顶层的业务代码是哪一行是persist、merge、remove还是flush。这决定了是哪个操作触发了事务要求。第二步检查调用链上有没有Transactional。从 Controller 到 Service 到 Repository逐层看。注意自调用、私有方法、final 方法这些代理不生效的情况。第三步检查事务管理器的配置。如果是多数据源项目确认当前操作的数据源和事务管理器是否匹配。看Transactional有没有指定transactionManager。第四步检查线程上下文。如果涉及异步、线程池、消息消费等场景确认事务上下文有没有正确传播。第五步检查测试配置。如果是测试环境确认测试类有没有加Transactional。4.2 修复方案对照表问题场景修复方案注意事项自调用导致代理失效拆分到不同 Bean或用TransactionTemplate拆 Bean 最干净但要注意循环依赖传播行为配置错误改为REQUIRED或REQUIRES_NEWREQUIRES_NEW会挂起外层事务慎用多数据源管理器不匹配显式指定transactionManager确认 Bean 名称拼写正确异步线程无事务异步方法上加Transactional失去外层事务原子性需评估业务影响测试类无事务测试类加Transactional默认回滚需要提交时加Commit只读事务做写操作去掉readOnly true只读事务不是“不能写”而是“不 flush”4.3 用 TransactionTemplate 手动控制事务当 AOP 代理搞不定的时候TransactionTemplate是最可靠的兜底方案。它不依赖代理直接通过编程方式开启和提交事务。Service public class OrderService { Autowired private TransactionTemplate transactionTemplate; Autowired private EntityManager entityManager; public void updateOrder(Long orderId) { transactionTemplate.execute(status - { Order order entityManager.find(Order.class, orderId); order.setStatus(PAID); entityManager.merge(order); return null; }); } }TransactionTemplate的execute方法接收一个TransactionCallback在回调里执行的操作都在事务中。如果回调抛出RuntimeException事务自动回滚正常返回则提交。这种方式的好处是事务边界非常清晰不受代理机制影响适合在复杂调用链或自调用场景中使用。配置TransactionTemplate也很简单注入PlatformTransactionManager即可Configuration public class TransactionConfig { Bean public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { return new TransactionTemplate(transactionManager); } }如果需要指定传播行为或超时时间可以在创建TransactionTemplate时设置TransactionTemplate template new TransactionTemplate(transactionManager); template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); template.setTimeout(30); // 30秒超时4.4 异步场景下的事务传播方案如果业务确实需要在异步线程里做写操作又希望有一定的原子性保障可以考虑以下几种方案方案一异步方法独立事务。每个异步方法加Transactional各自独立提交。适合批量处理中单条失败不影响其他的场景。方案二事务同步回调。用TransactionSynchronizationManager.registerSynchronization()在事务提交后触发异步操作。这样异步操作在事务成功后才执行但异步操作本身不在原事务中。Transactional public void processWithCallback(Data data) { entityManager.persist(data); TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { Override public void afterCommit() { asyncService.notifyExternal(data); } } ); }方案三消息队列解耦。事务提交后发消息到队列消费者异步处理。这是最彻底的解耦方案但引入了消息中间件的复杂度。4.5 日志与监控配置排查事务问题日志是关键。建议在开发环境开启以下日志级别logging: level: org.springframework.transaction: DEBUG org.springframework.orm.jpa: DEBUG org.hibernate.engine.transaction: DEBUG开启后日志里会输出事务的开启、提交、回滚、挂起等详细信息以及EntityManager的绑定和解绑过程。比如看到Creating new transaction with name [xxx]说明事务开启了看到Bound value [...] to thread说明资源绑定了。如果只看到方法调用但没有事务创建日志说明Transactional没生效。生产环境不建议开 DEBUG但可以配置一个慢事务或异常事务的监控切面记录事务执行时间和异常信息便于事后排查。5. 常见问题速查与避坑经验5.1 高频问题速查表现象可能原因快速验证方法加了Transactional仍报错自调用、代理不生效在方法入口打日志看是否经过代理查询正常保存报错查询不需要事务保存需要检查方法上有没有Transactional多数据源下部分方法报错事务管理器不匹配检查Transactional的transactionManager异步方法报错线程上下文丢失异步方法上加Transactional测试环境报错测试类无事务测试类加Transactional只读事务里报错写操作放进了只读事务检查readOnly配置事务提交后报错事务已结束EntityManager已关闭检查是否有延迟加载或异步操作5.2 几个容易忽视的细节Transactional加在接口上还是实现类上Spring 官方推荐加在实现类上。加在接口上时如果用的是 JDK 动态代理事务能生效但如果用的是 CGLIB 代理接口上的注解可能读不到。统一加在实现类上最稳妥。Transactional的方法必须是 public。protected、private、包级私有的方法上标注Transactional代理不会拦截。这是 Spring AOP 的基本限制不是 bug。Transactional和Async同时加在一个方法上。这两个注解的代理逻辑会互相干扰事务和异步的执行顺序不确定。正确做法是拆成两个方法一个负责事务一个负责异步通过外部调用串联。事务超时设置。Transactional(timeout 5)表示事务超过 5 秒自动回滚。如果某个操作耗时较长超时后事务被回滚后续操作就会报错。排查时要看日志里有没有Transaction timed out的提示。嵌套事务的保存点。PROPAGATION_NESTED会在当前事务中创建一个保存点内层事务回滚不影响外层。但这个模式需要事务管理器支持保存点JpaTransactionManager默认支持但某些数据库或连接池可能不支持。5.3 我踩过的几个坑坑一在PostConstruct方法里操作数据库。PostConstruct在 Bean 初始化时执行此时事务代理可能还没完全就绪而且这个方法本身不在事务中。如果里面调用了EntityManager的写操作必报此错。解决办法是把初始化逻辑放到ApplicationRunner或CommandLineRunner里或者用TransactionTemplate手动开启事务。坑二在Transactional方法里捕获异常后继续操作。前面提到过异常被吞掉后事务可能已被标记为 rollback-only后续操作会失败。更隐蔽的是如果捕获的是PersistenceException的子类事务拦截器可能已经标记了回滚但代码继续执行最终在提交时抛出UnexpectedRollbackException让人摸不着头脑。坑三用EntityManager的原生 API 但没配JpaTransactionManager。有些项目用DataSourceTransactionManager管理事务但代码里用EntityManager做持久化。DataSourceTransactionManager只绑定Connection不绑定EntityManager所以EntityManager代理找不到事务。解决办法是换成JpaTransactionManager或者用EntityManagerFactoryUtils手动获取EntityManager。坑四事务方法里开了新线程但没传播上下文。除了Async手动new Thread()或提交到ExecutorService也会丢失事务上下文。如果新线程里需要操作数据库必须在新线程里独立开启事务或者用TransactionSynchronizationManager手动传递资源不推荐容易出错。坑五Spring Boot 版本升级后事务行为变化。不同版本的 Spring 对事务默认行为的处理有细微差异比如readOnly的默认值、代理方式的默认选择等。升级后如果突然出现这个异常先对比版本变更日志看有没有事务相关的改动。6. 从设计层面避免这类问题6.1 事务边界的设计原则事务边界应该放在 Service 层而不是 Repository 层或 Controller 层。Repository 层的方法通常只做单表操作不需要独立事务Controller 层负责参数校验和响应组装不应该包含业务事务。Service 层的方法代表一个完整的业务用例事务边界在这里最合适。一个 Service 方法对应一个事务方法内的所有数据库操作要么全成功要么全失败。如果某个操作需要独立事务比如记录日志用REQUIRES_NEW拆出去。如果某个操作可以容忍失败用try-catch包起来并记录日志但要注意事务回滚标记的问题。6.2 代码规范上的约束团队里可以约定几条硬性规则来减少这类问题所有写操作的 Service 方法必须标注Transactional不允许依赖调用方的事务Transactional只加在 public 方法上加在实现类上禁止在 Service 内部自调用事务方法需要复用的逻辑抽到独立的 Bean多数据源项目里Transactional必须显式指定transactionManager异步方法如果需要操作数据库必须自己管理事务测试类统一加Transactional需要提交的用Commit标注这些规则看起来繁琐但能避免 90% 以上的事务相关问题。代码审查时重点检查这几条比事后排查异常高效得多。6.3 监控与告警生产环境里事务异常应该纳入监控。可以配置一个ControllerAdvice或RestControllerAdvice全局异常处理器捕获TransactionRequiredException和TransactionException记录详细的上下文信息当前用户、请求路径、调用链并触发告警。RestControllerAdvice public class TransactionExceptionHandler { private static final Logger log LoggerFactory.getLogger(TransactionExceptionHandler.class); ExceptionHandler(TransactionRequiredException.class) public ResponseEntityString handleTransactionRequired(TransactionRequiredException e) { log.error(事务缺失异常请检查事务配置, e); return ResponseEntity.status(500).body(系统繁忙请稍后重试); } }告警不是目的目的是通过异常发生的频率和场景发现代码里潜在的事务配置问题。如果某个接口频繁触发这个异常说明那里的事务边界设计有问题需要重构。事务管理是 JPA 和 Spring 集成中最容易出问题的环节之一但一旦理解了代理机制和线程绑定的原理排查起来就有章可循。我个人的经验是遇到这个异常先看调用链再看代理最后看线程三步之内基本能定位。真正难缠的不是异常本身而是那些“看起来有事务但实际上没有”的隐蔽场景这需要平时在代码规范上多下功夫而不是等到报错了再救火。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进