背景
之前工作中有个需求,需要对接第三方的财务系统,他们提供一个前置库,我们定时把数据推送至前置库,于是就想到用baomidou的dynamic datasource,它可以配置连接多个数据源,同时在使用数据源的时候通过@DS随意切换一个数据源,因为我们系统是多租户的,刚开始对接第一家租户时没什么问题,慢慢的,有多家机构需要对接同样的财务系统,而且财务系统是每个租户一套前置库,租户与租户之间网络是不互通的,于是就想着把数据库的配置改成租户参数,根据参数动态的创建数据源,后面新的租户对接只需页面添加数据库配置即可
实现步骤
- 在主流程开始时,往dynamicRoutingDataSource 添加数据源
//使用ThreadLocal保存当前租户标识
ThreadLocalOrg.set(syncParam.getOrgId());
//创建druid数据源
dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl(paramMap.get(ParamsConstants.FMS_DB_URL).getVal().trim());
dataSource.setUsername(paramMap.get(ParamsConstants.FMS_DB_USER).getVal().trim());
dataSource.setPassword(paramMap.get(ParamsConstants.FMS_DB_PASSWORD).getVal().trim());
dataSource.setBreakAfterAcquireFailure(true);//失败不重试
//往DynamicRoutingDataSource添加数据源,key为租户标识
dynamicRoutingDataSource.addDataSource(syncParam.getOrgId(),dataSource);
- 自定义切换数据源注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DS_ORG {
}
- 添加切面,实现根据租户id动态切换数据源
@Aspect
@Component
public class DataSourceOrgAspect {
@Around("@annotation(net.jqsoft.datasync.anno.DS_ORG)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取租户标识
String dataSourceKey = ThreadLocalOrg.get();
DynamicDataSourceContextHolder.push(dataSourceKey);
try {
return joinPoint.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
}
- 整个同步任务结束后,移除当前租户的数据源,并关闭数据源,释放资源
dynamicRoutingDataSource.removeDataSource(syncParam.getOrgId());
if (Objects.nonNull(dataSource) && !dataSource.isClosed()) {
dataSource.close();
}
优点
- 灵活扩展性:支持多租户系统、数据分片、读写分离等复杂场景,无需重启应用即可动态接入新数据源,提升业务适应性
- 资源优化: 延迟创建连接池,仅在首次使用时初始化,减少未使用数据源的资源占用
- 动态管理能力:支持运行时根据业务需求切换数据源,例如通过注解或代码动态路由,增强系统灵活性
缺点
- 事务管理风险:动态切换可能导致事务上下文不一致,尤其跨数据源操作时易引发事务失效或数据不一致,需额外处理分布式事务
- 性能开销:频繁创建和销毁连接池会增加系统开销,高并发场景下可能影响响应速度,需合理设计连接池复用策略
- 监控调试困难:频繁创建和销毁连接池会增加系统开销,高并发场景下可能影响响应速度,需合理设计连接池复用策略