Java(基于AOP的动态数据源切换(附源码))

1 动态数据源的必要性 我们知道,物理服务机的CPU、内存、存储空间、连接数等资源都是有限的,某个时段大量连接同时执行操作,会导致数据库在处理上遇到性能瓶颈。而在复杂的互联网业务场景下,系统流量日益膨胀。为了解决这个问题,行业先驱门充分发扬了分而治之的思想,对大库表进行分割,然后实施更好的控制和管理,同时使用多台机器的CPU、内存、存储,提供更好的性能。参考我这篇《分库分表》。
数据库有水平拆分(Scale Out) 和垂直拆分(Scale Up)的区别,但是无论怎么变化,当你对同一业务库进行分库的时候。必然要考虑到,在你的同一个业务服务(Service),会有同时访问多个数据源的情况。如下图
Java(基于AOP的动态数据源切换(附源码))
文章图片

另外一种场景是ABTesting业务场景,可能不同的用户看到的业务数据是不一样的,这就需要根据业务特性动态的获取数据。
按照Spring boot的常规做法,maven添加依赖,在Yaml中配置对应的datasource、jpa等属性即可使用了。但是多数据源的情况下无论是配置 还是数据上下文的切换都变得无比繁琐。如果能使用注解声明的方式,粒度细化到方法级别的,那用起来就简单多了。那我们来写一个这样的实现。
2 实现过程 2.1 Maven依赖 pom文件中增加一些依赖,这边我们以Jpa为案例说明:

org.springframework spring-jdbc 4.3.8.RELEASE com.alibaba druid-spring-boot-starter 1.1.20 org.springframework.boot spring-boot-starter-data-jpa byte-buddy net.bytebuddy hibernate-entitymanager org.hibernate hibernate-core org.hibernate com.querydsl querydsl-jpa com.querydsl querydsl-apt org.hibernate hibernate-core 5.3.7.Final com.vladmihalcea hibernate-types-52 2.9.7 org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java runtime

2.2 yaml配置 可以看到我们配置了一个默认的数据源basic,然后再扩展了一个跟basic同级的节点mutil-data-core,包含三个数据源,basic、cloudoffice、attend。
spring: mutildata: basic: driver-class-name: com.mysql.jdbc.Driver filters: stat initial-size: 20 logAbandoned: true maxActive: 300 maxPoolPreparedStatementPerConnectionSize: 20 maxWait: 60000 min-idle: 5 minEvictableIdleTimeMillis: 300000 poolPreparedStatements: true removeAbandoned: true removeAbandonedTimeout: 1800 testOnBorrow: false testOnReturn: false testWhileIdle: true timeBetweenEvictionRunsMillis: 60000 validationQuery: SELECT 1 password: 123456 type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/basic?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true username: root mutil-data-core: basic: password: 123456 url: jdbc:mysql://127.0.0.1:3306/basic?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true username: root cloud: password: 123456 url: jdbc:mysql://127.0.0.1:3307/cloudoffice?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true username: root attend: password: 123456 url: jdbc:mysql://127.0.0.1:3308/attend?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true username: root

2.3 编写配置类Configuration 扫描我们上面的配置,spring.mutildata.basic下面的默认数据源,以及 mutil-data-core下面的多个动态数据源,有多少个扫描多少个出来,并进行组装,放到一个数据源map集合中:dataSourceMap。
@Bean(name = "basicDataSource") @ConfigurationProperties(prefix = "spring.mutildata.basic") // 这是我们动态数据源的配置位置 public DruidDataSource basicDataSource() { return new DruidDataSource(); }@Autowired private DataSourceCoreConfig dataSourceCoreConfig; /** * 动态集成可选的数据库路由,改掉之前硬编码的方式 * @param basicDataSource * @return */ @Bean(name = "routingDataSource") @Primary public RoutingDataSource routingDataSource(DruidDataSource basicDataSource) { RoutingDataSource routingDataSource = new RoutingDataSource(); Map dataSourceMap = new HashMap<>(16); HashMapmutildatacore = dataSourceCoreConfig.getMutilDataCore(); routingDataSource.setDefaultTargetDataSource(basicDataSource); try { Iterator iter = mutildatacore.entrySet().iterator(); while (iter.hasNext()) { // 轮询出所有的动态数据源 Map.Entry entry = (Map.Entry) iter.next(); String key = entry.getKey().toString(); DataSourceCore dsc = (DataSourceCore) entry.getValue(); DruidDataSource ds = (DruidDataSource) basicDataSource.clone(); // 3个核心关键数据源头重新赋值 ds.setUrl(dsc.getUrl()); ds.setUsername(dsc.getUserName()); ds.setPassword(dsc.getPassWord()); dataSourceMap.put(key, ds); } } catch (Exception ex) { // Todo } routingDataSource.setTargetDataSources(dataSourceMap); return routingDataSource; }

2.4 数据源集合 数据源的管理:包含组织数据源、读值、赋值、清空数据源等。
/** * @author brand * @Description: 动态数据源 * @Copyright: Copyright (c) 2021 * @Company: Helenlyn, Inc. All Rights Reserved. * @date 2021/12/16 10:33 上午 * @Update Time: * @Updater: * @Update Comments: */ public class DynamicDataSource extends AbstractRoutingDataSource { /** * 用来保存数据源与获取数据源 */ private static final ThreadLocal CONTEXT_HOLDER = new ThreadLocal(); /** * 构造,包含一个默认数据源,和一个数据源集合 * @param defaultTargetDataSource * @param targetDataSources */ public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) { super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(new HashMap(targetDataSources)); super.afterPropertiesSet(); }@Override protected Object determineCurrentLookupKey() { return getDataSource(); }public static void setDataSource(String dataSource) { CONTEXT_HOLDER.set(dataSource); }public static String getDataSource() { return CONTEXT_HOLDER.get(); }public static void clearDataSource() { CONTEXT_HOLDER.remove(); } }

2.5 按键查找 无注解的情况下,lookupKey是空的,这边直接提供默认数据源。
有注解的时候,按照注解中的信息进行查找。
/** * 根据 lookupkey 获取到真正的目标数据源 * @return */ @Override protected DataSource determineTargetDataSource() { Assert.notNull(this.targetDataSources, "DataSource router not initialized"); Object lookupKey = this.determineCurrentLookupKey(); DataSource dataSource = (DataSource) this.targetDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) {// 无注解的情况下,lookupKey是空的,会走到这边,这时候给默认值 dataSource = this.resolvedDefaultDataSource; }if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } else { return dataSource; } }

2.6 初始化后的数据源结构 注意它的key,跟我们配置中的一模一样,basic、cloudoffice、attend。这个很重要,注解用这个来匹配。
Java(基于AOP的动态数据源切换(附源码))
文章图片

2.7 编写Annotation 写一个注解,映射的目标范围为 类型和方法。
/** * @author brand * @Description: 数据源切换注解 * @Copyright: Copyright (c) 2021 * @Company: Helenlyn, Inc. All Rights Reserved. * @date 2021/12/15 7:36 下午 */ @Target({ ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String name() default ""; }

2.8 编写AOP实现 编写切面代码,以实现对注解的PointCut。
/** * @author brand * @Description: * @Copyright: Copyright (c) 2021 * @Company: Helenlyn, Inc. All Rights Reserved. * @date 2021/12/15 7:49 下午 */ @Aspect @Component public class DataSourceAspect implements Ordered{ /** * 定义一个切入点,匹配到上面的注解DataSource */ @Pointcut("@annotation(com.helenlyn.dataassist.annotation.DataSource)") public void dataSourcePointCut() { }/** * Around 环绕方式做切面注入 * @param point * @return * @throws Throwable */ @Around("dataSourcePointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); DataSource ds = method.getAnnotation(DataSource.class); String routeKey = ds.name(); // 从头部中取出注解的name(basic 或 cloudoffice 或 attend),用这个name进行数据源查找。 String dataSourceRouteKey = DynamicDataSourceRouteHolder.getDataSourceRouteKey(); if (StringUtils.isNotEmpty(dataSourceRouteKey)) { // StringBuilder currentRouteKey = new StringBuilder(dataSourceRouteKey); routeKey = ds.name(); } DynamicDataSourceRouteHolder.setDataSourceRouteKey(routeKey); try { return point.proceed(); } finally { // 最后做清理,这个步骤很重要,因为我们的配置中有一个默认的数据源,执行完要回到默认的数据源。 DynamicDataSource.clearDataSource(); DynamicDataSourceRouteHolder.clearDataSourceRouteKey(); } }@Override public int getOrder() { return 1; } }

2.9 测试与效果 2.9.1 数据源key信息
数据源key 信息,有多少个数据源,这边就配置多少个,注意值须与yaml配置中的值保持一致。
/** * 数据源key 信息,有多少个数据源,这边就配置多少个, * 值须与yaml配置中的保持一致 */ public static final String DATA_SOURCE_BASIC_NAME = "basic"; public static final String DATA_SOURCE_ATTEND_NAME = "attend"; public static final String DATA_SOURCE_CLOUD_NAME = "cloud";

2.9.2 测试方法
在Control中写三个测试方法
/** * 无注解默认情况:数据源指向basic * @return */ @RequestMapping(value = "https://www.it610.com/default/{user_code}", method = RequestMethod.GET) public UserInfoDto getUserInfo(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode); }/** * 数据源指向attend * @return */ @DataSource(name= Constant.DATA_SOURCE_ATTEND_NAME) @RequestMapping(value = "https://www.it610.com/attend/{user_code}", method = RequestMethod.GET) public UserInfoDto getUserInfoAttend(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode); }/** * 数据源指向cloud * @return */ @DataSource(name= Constant.DATA_SOURCE_CLOUD_NAME) @RequestMapping(value = "https://www.it610.com/cloud/{user_code}", method = RequestMethod.GET) public UserInfoDto getUserInfoCloud(@PathVariable("user_code") String userCode) { return userInfoService.getUserInfo(userCode); }

2.9.3 效果
Java(基于AOP的动态数据源切换(附源码))
文章图片

Java(基于AOP的动态数据源切换(附源码))
文章图片

Java(基于AOP的动态数据源切换(附源码))
文章图片

3 总结和代码参考 【Java(基于AOP的动态数据源切换(附源码))】如果需要扩展数据源,在yaml的节点mutil-data-core下加配置数据就行了,简单方便。后面再写个MySQL的实现方式。
github代码:https://github.com/WengZhiHua/Helenlyn.Grocery/tree/master/parent/DynamicDataSource

    推荐阅读