AbstractRoutingDataSource单请求仅调用一次getConnection的分库问题
这个问题我之前做分库分表的时候也碰到过!核心原因其实是Spring在请求上下文里会复用EntityManager(也就是JPA的会话),一旦EntityManager绑定了某个数据源的连接,整个会话周期内就不会再去调用AbstractRoutingDataSource.getConnection()了——所以你在同一个请求里切换ThreadLocal的数据源key也没用,因为连接已经被固定死了。
下面给你两个经过实践验证的解决方案:
方案一:用事务传播属性强制开启新事务
利用Spring事务的REQUIRES_NEW传播属性,每次切换数据源时开启一个独立的新事务,这样新事务会重新触发数据源路由逻辑。
先确认你的基础配置是正确的(这里贴出来做参考):
// 数据源上下文Holder,用ThreadLocal存储当前数据源key public class DataSourceContextHolder { private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>(); public static void setDataSourceKey(String key) { DATA_SOURCE_KEY.set(key); } public static String getDataSourceKey() { return DATA_SOURCE_KEY.get(); } public static void clearDataSourceKey() { DATA_SOURCE_KEY.remove(); } } // 自定义路由数据源 public class ShardingRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSourceKey(); } }
然后在Service层拆分方法,给每个需要切换数据源的方法加上@Transactional(propagation = Propagation.REQUIRES_NEW):
@Service public class PlayerService { @Autowired private PlayerRepository playerRepo; // 操作分片A @Transactional(propagation = Propagation.REQUIRES_NEW) public void registerToShardA(Player player) { DataSourceContextHolder.setDataSourceKey("shard_a"); playerRepo.save(player); // 必须清理ThreadLocal,避免内存泄漏 DataSourceContextHolder.clearDataSourceKey(); } // 操作分片B @Transactional(propagation = Propagation.REQUIRES_NEW) public void registerToShardB(Player player) { DataSourceContextHolder.setDataSourceKey("shard_b"); playerRepo.save(player); DataSourceContextHolder.clearDataSourceKey(); } }
在Controller里直接调用这两个方法即可,每个方法都会开启新事务,触发数据源重新路由:
@RestController @RequestMapping("/players") public class PlayerController { @Autowired private PlayerService playerService; @PostMapping("/cross-shard") public ResponseEntity<Void> crossShardRegister(@RequestBody Player player) { playerService.registerToShardA(player); playerService.registerToShardB(player); return ResponseEntity.ok().build(); } }
方案二:手动创建EntityManager实例
如果不想依赖事务,或者需要更细粒度的控制,可以直接从EntityManagerFactory手动创建新的EntityManager——每次切换数据源后创建新实例,每个实例都会重新获取对应的数据源连接。
示例代码:
@Service public class PlayerService { @Autowired private EntityManagerFactory entityManagerFactory; public void crossShardOperation(Player playerA, Player playerB) { // 操作分片A DataSourceContextHolder.setDataSourceKey("shard_a"); try (EntityManager em = entityManagerFactory.createEntityManager()) { em.getTransaction().begin(); em.persist(playerA); em.getTransaction().commit(); } finally { DataSourceContextHolder.clearDataSourceKey(); } // 切换到分片B,创建新的EntityManager DataSourceContextHolder.setDataSourceKey("shard_b"); try (EntityManager em = entityManagerFactory.createEntityManager()) { em.getTransaction().begin(); em.persist(playerB); em.getTransaction().commit(); } finally { DataSourceContextHolder.clearDataSourceKey(); } } }
关键注意点
- 务必清理ThreadLocal:不管用哪种方案,都要在操作完成后调用
DataSourceContextHolder.clearDataSourceKey(),否则线程池场景下会出现内存泄漏问题。 - 不要在同一个EntityManager内切换数据源:EntityManager一旦绑定了连接就不会再改变,必须用新的EntityManager或新事务来切换。
- 事务传播属性的选择:声明式事务方案中,
REQUIRES_NEW是核心——它会暂停当前事务,开启完全独立的新事务,从而触发数据源重新路由。
内容的提问来源于stack exchange,提问作者humbaba
相关产品推荐
相关产品推荐

