spring那些事(1) : 自定义注解实现 Spring 多数据源

1,103 阅读5分钟

“持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

一、背景

在实际开发中,我们往往面临一个应用需要访问多个数据库的情况。例如下面两种场景。

  • 业务复杂:数据分布在不同的数据库,数据库拆了,应用没拆,一个公司有多个子项目,各用各的数据库。

  • 读写分离:为了解决数据库的读性能瓶颈(读比写性能更高,写锁会影响读阻塞,从而影响读的性能)

    很多数据库拥有主从架构,也就是说,一台主数据库服务器,是对外提供增删改查业务的生产服务器;

    另一台从数据库服务器,主要进行读的操作。

    读写分离:解决高并发下读写受影响。数据更新在主库上进行,主库将数据变更信息同步给从库。在查询时,在从库上进行,从而分担主库的压力。

我们可以在代码层面解决这种动态数据源切换的问题,而不需要使用mycat、shardingJDBC 等数据库插件。本文将主要以自定义注解+继承 AbstractRoutingDataSource 实现读写分离。

二、如何实现多数据源

2.1 AbstractRoutingDataSource 分析

想要自定义 动态数据源切换,得先了解这个类。

AbstractRoutingDataSource 是在spring2.0.1中引入的,该类充当了 DataSource 的路由中介,通过这个类来找到真正的数据源。

大致,我们需要提前准备好多个数据源,存入一个 map中,key 是这个数据源的名字,value 是具体的数据源,然后再把这个 Map 配置到 AbstractRoutingDataSource 中,最后,每次执行数据库查询的时候,拿 key 与map中比较,找到最终的数据源。

这个类有两个重要的成员变量。

  • targetDataSources :默认的数据源,子类需要重新设置
  • targetDataSources : 所有数据源集合。子类获取所有数据源,然后设置进去。

几个重要方法:

  • afterPropertiesSet : spring bean 对象初始化方法。
  • determineTargetDataSource : 获取指定的数据源。
 public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
     @Nullable
     private Map<Object, Object> targetDataSources;
     
     @Nullable
     private Object defaultTargetDataSource;
    
     public void setTargetDataSources(Map<Object, Object> targetDataSources) {
         this.targetDataSources = targetDataSources;
     }
 ​
     public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
         this.defaultTargetDataSource = defaultTargetDataSource;
     }
 ​
     public void afterPropertiesSet() {
         if (this.targetDataSources == null) {
             throw new IllegalArgumentException("Property 'targetDataSources' is required");
         } else {
             this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
             this.targetDataSources.forEach((key, value) -> {
                 Object lookupKey = this.resolveSpecifiedLookupKey(key);
                 DataSource dataSource = this.resolveSpecifiedDataSource(value);
                 this.resolvedDataSources.put(lookupKey, dataSource);
             });
             if (this.defaultTargetDataSource != null) {
                 this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
             }
 ​
         }
     }
 ​
     protected DataSource determineTargetDataSource() {
         Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
         Object lookupKey = this.determineCurrentLookupKey();
         DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
         if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
             dataSource = this.resolvedDefaultDataSource;
         }
 ​
         if (dataSource == null) {
             throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
         } else {
             return dataSource;
         }
     }
 }

2.2 实现思路

1、自定义一个注解 @DataSource,将来可以将该注解加在方法或者类上面,表示方法或者类中的所有方法都使用某一个数据源。

2、对于第一步,如果某个方法上面有 @DatsSource 注解,那么就将该方法需要使用的数据源名称存入 ThreadLocal

3、自定义切面,在切面中解析 @DataSource 注解。当一个方法或者类上面,有 @DataSource 注解的时候,将 @DataSource 注解所标记的数据源列出来存入到 ThreadLocal 中。

4、最后,当 Mapper 执行的时候,需要 DataSource,他会自动去 AbstractRoutingDataSource 类中查找需要的数据源,我们只需要在AbstractRoutingDataSource 中返回ThreadLocal 中的值就可以。

注:为什么要用ThreadLocal 呢,因为一方面考虑线程安全,另一方面也考虑全局可用。在service 层添加完注解后,在mapper层中也能识别到。

三、实现案例

3.1 创建项目

创建 springboot 项目,命名 dynamic-Datasource,引入 web、mybatis、mysql链接、druid、aop 等依赖。

 <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
         </dependency>
 ​
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
 ​
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-aop</artifactId>
         </dependency>
 ​
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
 ​
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>druid-spring-boot-starter</artifactId>
             <version>1.2.9</version>
         </dependency>
 ​
         <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>8.0.18</version>
         </dependency>
 ​
         <!--mybatis plus-->
         <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-boot-starter</artifactId>
             <version>3.0.1</version>
         </dependency>
 ​

然后创建配置文件 application.yml

 # 数据源配置
 spring:
     datasource:
         type: com.alibaba.druid.pool.DruidDataSource
         driverClassName: com.mysql.cj.jdbc.Driver
         ds:
             # 主库数据源
             master:
                 url: jdbc:mysql://192.168.51.101:3306/test01?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                 username: root
                 password: Admin@2021
             # 从库数据源
             slave:
                 url: jdbc:mysql://192.168.51.101:3306/test02?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                 username: root
                 password: Admin@2021
          # 初始连接数
         initialSize: 5
         # 最小连接池数量
         minIdle: 10
         # 最大连接池数量
         maxActive: 20
         # 配置获取连接等待超时的时间
         maxWait: 60000
         # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
         timeBetweenEvictionRunsMillis: 60000
         # 配置一个连接在池中最小生存的时间,单位是毫秒
         minEvictableIdleTimeMillis: 300000
         # 配置一个连接在池中最大生存的时间,单位是毫秒
         maxEvictableIdleTimeMillis: 900000
         # 配置检测连接是否有效
         validationQuery: SELECT 1 FROM DUAL
         testWhileIdle: true
         testOnBorrow: false
         testOnReturn: false
         webStatFilter:
             enabled: true
         statViewServlet:
             enabled: true
             # 设置白名单,不填则允许所有访问
             allow:
             url-pattern: /druid/*
             # 控制台管理用户名和密码
             login-username: giant
             login-password: 123456
         filter:
             stat:
                 enabled: true
                 # 慢SQL记录
                 log-slow-sql: true
                 slow-sql-millis: 1000
                 merge-sql: true
             wall:
                 config:
                     multi-statement-allow: true
 # 日志配置
 logging:
     level:
         com.xiaolei: debug
         org.springframework: warn

ds 中是我们的所有数据源。master 是默认的数据源,不可修改,其他的数据源可以修改并添加多个。

然后我们可以通过@ConfigurationProperties 注解加载定义的配置文件。spring.datasource 对应的注解都会匹配到。

 @ConfigurationProperties(prefix = "spring.datasource")
 public class DruidProperties {
     private String type;
     private String driverClassName;
     private Map<String, Map<String,String>> ds;
 ​
     private Integer initialSize;
     private Integer minIdle;
     private Integer maxActive;
     private Integer maxWait;
 ​
     /**
      *一会在外部构建好一个 DruidDataSource 对象,包含三个核心属性 url、username、password
      * 在这个方法中设置公共属性
      * @param druidDataSource
      * @return
      */
     public DataSource dataSource(DruidDataSource druidDataSource){
         druidDataSource.setInitialSize(initialSize);
         druidDataSource.setMinIdle(minIdle);
         druidDataSource.setMaxActive(maxActive);
         druidDataSource.setMaxWait(maxWait);
         return druidDataSource;
     }
     public String getType() {
         return type;
     }
 ​
     public void setType(String type) {
         this.type = type;
     }
 ​
     public String getDriverClassName() {
         return driverClassName;
     }
 ​
     public void setDriverClassName(String driverClassName) {
         this.driverClassName = driverClassName;
     }
 ​
     public Map<String, Map<String, String>> getDs() {
         return ds;
     }
 ​
     public void setDs(Map<String, Map<String, String>> ds) {
         this.ds = ds;
     }
 ​
     public Integer getInitialSize() {
         return initialSize;
     }
 ​
     public void setInitialSize(Integer initialSize) {
         this.initialSize = initialSize;
     }
 ​
     public Integer getMinIdle() {
         return minIdle;
     }
 ​
     public void setMinIdle(Integer minIdle) {
         this.minIdle = minIdle;
     }
 ​
     public Integer getMaxActive() {
         return maxActive;
     }
 ​
     public void setMaxActive(Integer maxActive) {
         this.maxActive = maxActive;
     }
 ​
     public Integer getMaxWait() {
         return maxWait;
     }
 ​
     public void setMaxWait(Integer maxWait) {
         this.maxWait = maxWait;
     }
 }

3.2 加载数据源

@EnableConfigurationProperties :这个注解的意思是使 ConfigurationProperties 注解生效。

 @Component
 @EnableConfigurationProperties(DruidProperties.class)
 public class LoadDataSource {
     @Autowired
     DruidProperties druidProperties;
 ​
     public Map<String, DataSource> loadAllDataSource()   {
         Map<String, DataSource> map =new HashMap<>();
         Map<String, Map<String, String>> ds = druidProperties.getDs();
         try {
             Set<String> keySet = ds.keySet();
             for (String key : keySet) {
                 map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
         return map;
     }
 }

该类中的 loadAllDataSource 方法可以读取所有数据源对象。

druidProperties.dataSource(DruidDataSource druidDataSource)这个方法为每个数据源配置其他额外的属性(最大连接池等)

DruidDataSourceFactory.createDataSource(ds.get(key):创建一个数据源,赋予三个核心的属性。(username、url、password)

最终,所有的数据源都存入map中。

3.3 数据源切换

3.3.1 定义 threadLocal 工具类

 public class DynamicDataSourceUtil {
 ​
     private static ThreadLocal<String> CONTEXT_HOLDER =new ThreadLocal<>();
 ​
     public static void setDataSourceType(String dsType){
         CONTEXT_HOLDER.set(dsType);
     }
     public static String getDataSourceType(){
         return CONTEXT_HOLDER.get();
     }
 ​
     public static void clear(){
         CONTEXT_HOLDER.remove();
     }
 }

threadLocal 可以确保多线程环境下的数据安全。

3.3.2 自定义注解

定义默认数据源常量

 public interface DataSourceType {
     String default_ds_name ="master";
 }
 /**
  *
  * 这个注解将来可以加在某一个 service 类上或者方法上,通过 value 属性来指定类或者方法应该使用哪个数据源
  * @author xiaolei
  * @version 1.0
  * @date 2022-05-15 10:07
  */
 @Retention(RetentionPolicy.RUNTIME)
 @Target({ElementType.TYPE,ElementType.METHOD})
 public @interface DataSource {
     /**
      * 如果一个方法上加了 @DataSource 注解,但是却没有指定数据源的名称,那么默认使用 Master 数据源
      * @return
      */
     String value() default DataSourceType.default_ds_name;
 }

3.3.3 AOP 解析自定注解

 @Component
 @Aspect
 public class DataSourceAspet {
 ​
     /**
      * @annotation(com.xiaolei.dynamicdatasource.annotation.DataSource) 表示方法上有 @DataSource 注解 就将方法拦截下来。
      * @within :如果类上面有 @DataSource 注解,就将类中的方法拦截下来。
      */
     @Pointcut("@annotation(com.xiaolei.dynamicdatasource.annotation.DataSource) || " +
             "@within(com.xiaolei.dynamicdatasource.annotation.DataSource)")
     public void pc(){
 ​
     }
 ​
     @Around("pc()")
     public Object around(ProceedingJoinPoint point){
         //获取方法上面的注解
         DataSource dataSource =getDataSource(point);
         if(dataSource!=null){
             // 注解中数据源的名称
             String value = dataSource.value();
             DynamicDataSourceUtil.setDataSourceType(value);
         }
         try {
             return point.proceed();
         } catch (Throwable throwable) {
             throwable.printStackTrace();
         }finally {
             DynamicDataSourceUtil.clear();
         }
         return null;
     }
 ​
     private DataSource getDataSource(ProceedingJoinPoint point) {
         /**
          * 先去查找方法上的注解,如果没有,再去类中找。
          */
         MethodSignature signature = (MethodSignature)point.getSignature();
         DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
         if(annotation!=null){
             return annotation;
         }
         return AnnotationUtils.findAnnotation(signature.getDeclaringType(),DataSource.class);
     }
 }

1、首先在 pc()方法定义了其欸但,可以拦截下所有带有 @DataSource 注解的方法,同时这个注解可以加在类上和方法上,如果是类上,就代表类中所有方法都使用该数据源。

2、接下来我们定义一个环绕通知,首先从切点进入方法,获取@DataSource注解。如果注解为空,就不设置数据源。

3、方法调用完成后,需要清除 threadLocal 的数据。

3.4 定义动态数据源

 @Component
 public class DynamicDataSource extends AbstractRoutingDataSource {
 ​
     public DynamicDataSource(LoadDataSource loadDataSource) {
         // 1、设置所有的数据源
         Map<String, DataSource> stringDataSourceMap = loadDataSource.loadAllDataSource();
         super.setTargetDataSources(new HashMap<>(stringDataSourceMap));
         // 2、设置默认的数据源
         super.setDefaultTargetDataSource(stringDataSourceMap.get(DataSourceType.default_ds_name));
 ​
         super.afterPropertiesSet();
     }
 ​
     /**
      * 这个方法用来返回数据源名称,当系统需要获取数据源的时候,会自动调用该方法获取数据源的名称
      * @return
      */
     @Override
     protected Object determineCurrentLookupKey() {
         return DynamicDataSourceUtil.getDataSourceType();
     }
 }

3.5 测试

定义一个 UserMapper

 @Mapper
 public interface UserMapper {
 ​
     @Select("select * from user")
     List<User> getAll();
 }

再来一个 service

 @Service
 public class UserService {
     @Autowired
     private UserMapper userMapper;
 ​
     @DataSource("slave")
     public List<User> getAll(){
         List<User> all = userMapper.getAll();
         return all;
     }
 }

单元测试

 @SpringBootTest
 class DynamicDatasourceApplicationTests {
 ​
     @Autowired
     private UserService userService;
 ​
     @Test
     void contextLoads() {
         List<User> all = userService.getAll();
         if(all !=null){
             for (User user : all) {
                 System.out.println(user);
             }
         }
     }
 ​
 }