You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
        }
    }
}

关键注意点

  1. 务必清理ThreadLocal:不管用哪种方案,都要在操作完成后调用DataSourceContextHolder.clearDataSourceKey(),否则线程池场景下会出现内存泄漏问题。
  2. 不要在同一个EntityManager内切换数据源:EntityManager一旦绑定了连接就不会再改变,必须用新的EntityManager或新事务来切换。
  3. 事务传播属性的选择:声明式事务方案中,REQUIRES_NEW是核心——它会暂停当前事务,开启完全独立的新事务,从而触发数据源重新路由。

内容的提问来源于stack exchange,提问作者humbaba

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 09:46:56