查看原文
其他

技术分享|Java SDK动态数据源和上下文机制

周同学 明源云天际PaaS平台 2022-09-10

源宝导读:在很多应用场景中,我们需要动态切换数据源,比如业务分库(不同业务访问不同数据库),多租户场景(不同租户数据库隔离),单库数据量很大影响到性能时,都可以使用动态数据源方案来解决,接下来我们就来讲解如何实现动态数据源以及实现原理。

一、背景

随着天际平台的诞生,承接了整个公司的技术底座。建模也同样需要从只支持ERP的产品线,扩充到支撑公司SaaS类业务。为了更多产品,客户能用上建模,享受低代码能力带来的便捷,我们引入Java SDK作为低代码的后端开发语言。配合上原有的.Net Core的改造,完成整个建模跨平台,跨语言开发,和容器化部署。

在实际的业务中:

1.数据量迅速增长,单库无法满足业务的需求,需要考虑分库的操作;

2.在多租户场景下,不同的租户需要使用不同的数据库来做到资源隔离;

3.在业务集中部署的情况下,不同业务本身就是连接的不同数据库,需要在调用不用的业务系统使用不同的数据库。

一般的业务需求可以根据业务规划自己做分库,但是平台层面应该考虑通用的切库方案,需要做到分库的配置化,可扩展,支持SASS等功能。

本篇着眼于 Java SDK 的相关经验,在动态数据源和上下文机制上做出一些分享和探讨。


二、常用方案

Shardingsphere

比较完整的分库分表方案,一般是通过ShardingSphere-JDBC和ShardingSphere-Proxy来处理,主要是来解决数据量大,解决应用增长带来的问题,运维成本较高,在业务体量没有达到时使用会增加较大成本。

ShardingSphere-JDBC:

ShardingSphere-Proxy:

MultiDataSource

一般来说方案如下:

1.配置文件中新增多个数据源(datasource)配置

2.在Configuration中为每个数据源注册各自的SessionFactory,TransactionManager,datasource

3.在代码中根据不同的规则,切换不同的datasource,一般有如下三种切换规则:

1)根据业务分包,不同的jar包使用不同的数据源(datasource)

2)通过注解来切换数据源(datasource),比如使用一个自定义注解,根据这个注解的内容去获取对应的数据源(datasource)

3)通过上下文来切换数据源(datasource),通过设置不同threadlocal,根据threadlocal的内容获取对应的数据源(datasource)

方案对比


分库

分表

学习成本

运维成本

扩展性

Shardingsphere

支持支持

MultiDataSource

支持不支持


三、解决方案

为了解决切库的痛点,解决SASS场景下租户数据库隔离,又不想使用如Shardingsphere这么复杂需要运维的组件的时候,我们需要一套可灵活扩展,方便且通用的切库方案,下面介绍一下我们在应用过程中的一些思路:

整体说明

如果是单独的业务系统,可以根据自己具体的业务需求设计出固定的分库逻辑,但是作为平台来讲,需要遵从通用性,可扩展,方便等原则。整体思路如下:

1.将分库规则做到可配置,这样方便用户根据业务的发展,方便调整分库规则。

2.分库规则依赖上下文进行传递,分库规则可以通过一些迭代器等设计模式实现可扩展。

3.在Service层能灵活的根据分库规则修改上下文信息,从而实现跨Service层切库逻辑(实现不同service使用不同数据库)。

4.当用户希望在某一块代码,固定使用某一个数据库的时候,可以通过注解的方式,修改上下文,进行手动切库。

切库配置化

切库配置化整体来说还是具有一定的业务属性的,简而言之,就是根据一些属性(appcode,租户编码等)去确定一个数据库连接字符串:

在建模目前是使用的配置中心来实现的,常用的场景如下:

1.在传统的ERP场景,主要是使用的应用编码分库(appcode),后续也会拓展,按照业务单元,公司ID等等做分库。

2.在SASS场景下,根据租户CODE等进行分库

我们目前是使用一个DbRouter类去做数据库选择的pojo类,用户可以通过重写该类,扩展新的类型来实现。

public class DbRouter implements Cloneable { /** * 运行状态 */ private RunStatus runStatus;
/** * 租户编码 */ private String tenantCode;
/** * 公司ID */ private String companyId;
/** * 应用CODE */ private String appCode;}

迭代器模式

一般来说分库的配置信息(DbRouter类:租户CODE,公司ID等)来源是不固定的,比如可以从Http请求的header里面传递,也可以从Http请求的参数来传递,也可以从包路径来获取,DbRouter里面的属性不是一次性获取的,属性填充是一个依次获取的内容,目前我们是采用迭代器模式来实现的。

上图描述了整个从请求进入(controller/service层),到生成上下文,到生成动态数据源的流程。

将上面的resolver在spring启动的时候注册到List<Resolver>中,用户有新的拓展,只需要实现一个新的Resolver注册到spring中即可。

封装了一个ResolverProvider方法,提供切库的方法,如下:

public class ResolverProvider {
/** * 拦截器resolvers */ private List<InterceptResolver> interceptResolvers;
/** * AOP的resolvers */ private List<AopResolver> aopResolvers;
/** * 手动的resolvers */ private ManualResolver manualResolver;
/** * analysisInterceptResolver 拦截器生成dbrouter * * @param request */ public void analysisRequest(HttpServletRequest request) { interceptResolvers.stream().forEach(key -> key.generateDbRouter(dbRouter, request)); }
/** * analysisAopResolver AOP方法生成dbrouter * * @return DbRouter */ public void analysisInvocation(MethodInvocation methodInvocation) { aopResolvers.stream().forEach(key -> key.generateDbRouter(dbRouter, methodInvocation)); }
/** * 手动设置dbRouter上下文 * * @param manualRouter 手动信息 * @return 返回 */ public DbRouter analysisDbRouter(DbRouter manualRouter) { DbRouter dbRouter = ThreadBusinessContext.getDbRouter(); // 设置dbRouter manualResolver.generateDbRouter(dbRouter, manualRouter); // 返回 return dbRouter; }
}

上下文切库逻辑

上面的实例demo可以看出,提供了InterceptResolver,AopResolver,ManualResolver几种类型的实现

InterceptResolver:基于拦截器的实现,处理HTTP请求的切库信息,埋点在param或者header中的信息

AopResolver:基于AOP的实现,处理包路径的切库信息,例如不同的包区分不同的APPCODE或者业务单元编码等

ManualResolver:手动切库,用户自定义的一些切库信息

在service层之间互相调用的时候,如果是跨服务之间的调用(RPC),当链路比较长,需要频繁的切库,为了解决这个问题,在AOP层做了一个栈的结构,通过出入栈实现了service之间调用的切库逻辑。

如下 AService BService  CService  分别是不同的三个业务系统的服务,分别要连接A,B,C三个自己的库。

@Servicepublic class CService extends AppService {
@Autowired private Aservice aservice;
@Autowired private Bservice bservice;
public void changeDb(){ //切换到C库,查询C的列表 findClist(); //切换到A库,查询A的列表 aservice.findAlist(); //重新切换到C库,查询C的列表 findClist(); //切换到B库,查询B的列表 bservice.findBlist(); //重新切换到C库,查询C的列表 findClist(); }
}

可以发现,需要通过AOP做好切面,在每个service的进入和推出做好切库的操作,比如进入CService 时需要把上下文切换到C库,进入AService 后,需要把上下文切换到A库,从Aservice执行完成后,需要重新切换回CService 需要的C库,直到整个调用结束完成,是不是特别像一个出入栈的操作,因此这里我们也是通过栈来解决这个问题。再进一步发现,如果我们把这个出入栈维护好以后,我们的调用链其实也完成了。

如下图:

解决问题的场景


四、局限及其优化思考

局限

现今 Java SDK 的实现方式存在有一定的局限性,其主要如下:

1.实现方式是依赖于栈结构,这样的话对数据的顺序有强要求,也就是说在多线程场景下会有问题(比如Aservice多线程调用Bserice,不是多线程调用service而是service内部多线程时是没有问题的);

2.现在的实现方式,dbrouter里面的内容还是通过手动编码(加字段)的形式进行拓展的(也和现在使用ERP业务相关),在平台层来看不够灵活,更好的方案应该是一个多维度可扩展的结构,dbrouter本身就是一个抽象的概念,里面的分库是可以函数式编程实现多维度的组合,需要完全做到零代码。

优化思考

整体来看目前这个方案主要还是分库,也可以解决SASS资源隔离等问题,如果说需要做到分表等,那么整体复杂度就会有很大的提高,但是随着后面SASS化,性能问题凸显的时候,我们也会考虑集成一些比较好的分库方案来实现,目前的思路是可以通过hibernate的EmptyInterceptor来实现分表的逻辑。

------ END ------

作者简介
周同学: 研发工程师,目前负责建模平台相关研发工作。

也许您还想看:
技术分享|Java SDK 动态类型
技术分享 | jaeger链路日志实现

更多明源云·天际开放平台场景案例与开发小知识,可以关注明源云天际开发者社区公众号:

繁星计划第五期(北京站),你真的了解明源云·天际开放平台吗?

天际·开发者社区“重装发布”!

建模零代码之建模账号接入DevOps 账号体系

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存