性能文章>Java:基于AOP的动态数据源切换(附源码)>

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

2月前
253326

1 动态数据源的必要性


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

另外一种场景是ABTesting业务场景,可能不同的用户看到的业务数据是不一样的,这就需要根据业务特性动态的获取数据。

按照Spring boot的常规做法,maven添加依赖,在Yaml中配置对应的datasource、jpa等属性即可使用了。但是多数据源的情况下无论是配置 还是数据上下文的切换都变得无比繁琐。如果能使用注解声明的方式,粒度细化到方法级别的,那用起来就简单多了。那我们来写一个这样的实现。

2 实现过程


2.1 Maven依赖

pom文件中增加一些依赖,这边我们以Jpa为案例说明:

<!-- 增加了4.3.8版本jdbc的支持-->
<dependency>
     <groupId>org.springframework</groupId>
     <artifactId>spring-jdbc</artifactId>
     <version>4.3.8.RELEASE</version>
</dependency>
 <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.20</version>
</dependency>

<!-- Jpa与Hibernate相关:开始-->
 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>byte-buddy</artifactId>
                    <groupId>net.bytebuddy</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>hibernate-entitymanager</artifactId>
                    <groupId>org.hibernate</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>hibernate-core</artifactId>
                    <groupId>org.hibernate</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.3.7.Final</version>
        </dependency>
        <dependency>
            <groupId>com.vladmihalcea</groupId>
            <artifactId>hibernate-types-52</artifactId>
            <version>2.9.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
<!-- Jpa与Hibernate相关:结束-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

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<Object, Object> dataSourceMap = new HashMap<>(16);
        HashMap<String, DataSourceCore>  mutildatacore = 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<String> CONTEXT_HOLDER = new ThreadLocal<String>();

    /**
     * 构造,包含一个默认数据源,和一个数据源集合
     * @param defaultTargetDataSource
     * @param targetDataSources
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<String, DataSource> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(new HashMap<Object, Object>(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。这个很重要,注解用这个来匹配。
image

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 = "/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 = "/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 = "/cloud/{user_code}", method = RequestMethod.GET)
    public UserInfoDto getUserInfoCloud(@PathVariable("user_code") String userCode) {
        return userInfoService.getUserInfo(userCode);
    }

2.9.3 效果

image


image


image

3 总结和代码参考


如果需要扩展数据源,在yaml的节点mutil-data-core下加配置数据就行了,简单方便。后面再写个MySQL的实现方式。
github代码:https://github.com/WengZhiHua/Helenlyn.Grocery/tree/master/parent/DynamicDataSource

欢迎关注公众号【架构与思维】:撰稿者为bat、字节的几位高阶研发/架构。不做广告、不卖课、不要打赏,只分享优质技术。

★ 加公众号获取学习资料和面试集锦

请先登录,再评论

文章写的很好

1月前

👍👍👍

2月前

为你推荐

字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
据说99.99%的人都会答错的类加载的问题
概述首先还是把问题抛给大家,这个问题也是我厂同学在做一个性能分析产品的时候碰到的一个问题。 同一个类加载器对象是否可以加载同一个类文件多次并且得到多个Class对象而都可以被java层使用吗请仔细注意
Java多线程——并发测试
编写并发程序时候,可以采取和串行程序相同的编程方式。唯一的难点在于,并发程序存在不确定性,这种不确定性会令程序出错的地方远比串行程序多,出现的方式也没有固定规则。那么如何在测试中,尽可能的暴露出这些问
Java多线程知识小抄集(一)
本文主要整理笔者遇到的Java多线程的相关知识点,适合速记,故命名为“小抄集”。本文没有特别重点,每一项针对一个多线程知识做一个概要性总结,也有一些会带一点例子,习题方便理解和记忆。 1.interr