专业的编程技术博客社区

网站首页 > 博客文章 正文

spring使用技巧-如何实现动态数据源

baijin 2024-10-03 17:34:44 博客文章 4 ℃ 0 评论

一、基本介绍

1.什么是动态数据源?

动态数据源是指在应用程序运行时,根据需求动态切换数据源的机制。传统的数据源配置通常是在应用程序启动时静态地配置好,而动态数据源则允许在运行时根据不同的条件或场景来选择使用不同的数据源。

2.什么场景下需要使用动态数据源

分库分表: 当应用程序需要处理大量的数据,需要将数据分散存储在多个数据库或数据表中时,可以通过动态数据源切换来实现对不同数据库实例或数据表的访问。

读写分离: 在高并发读写场景下,可以通过动态数据源切换来实现读写分离。读操作可以路由到只读数据库实例,而写操作可以路由到主数据库实例,以提升系统的读取性能和扩展性。

多租户应用: 在多租户架构中,每个租户可以拥有自己的独立数据源。通过动态数据源切换,可以根据当前用户的身份或租户信息选择相应的数据源,确保不同租户的数据隔离。

跨地域访问: 当应用程序需要跨地域或跨网络访问数据时,可以通过动态数据源切换来选择合适的数据源。根据用户所在地区或网络条件,选择就近的数据中心或数据源,以提供快速的数据访问。

二、如何实现动态数据源

1.功能组成


2.功能介绍

① 数据源切换策略

首先确定我们动态切换数据源的使用策略,一般我们在调用方法上添加数据源注解决定使用哪种数据源,如果没有指定,那么方法使用默认的数据源,使用示例下图所示:

② 动态数据源切换

在客户端发起某个请求时,我们需要在实际调用的方法前后使用 AOP 拦截切换数据源,在方法执行前,获取数据源并保存至请求上下文中,方法执行时,会根据自定义路由,获取请求上下文保存的数据源;方法执行后,最后需要清空上下文保存的数据源。执行流程如下图所示:

③ 数据源路由

Spring 中 提供了 数据源路由抽象类 AbstractRoutingDataSource,该类实现了 javax.sql.DataSource 接口,并且提供了一个抽象方法 determineCurrentLookupKey(),该方法返回当前线程需要使用的数据源标识。

我们可以继承 AbstractRoutingDataSource 类后,重写 determineCurrentLookupKey() 方法,获取通过 AOP 拦截保存在当前请求线程中的数据源并返回。通过这种方式,可以实现数据源的路由选择。示例代码如图所示:

④ 配置多数据源

最后需要支持从 applicaiton.yml 或application.properties 读取多个数据源配置信息,并将其分别配置成数据源。

三、代码实战

1.创建项目并引入依赖

  • pom.xml

2.实现数据源切换策略(自定义注解)

  • com.shawn.spring.tool.datasource.enums.DataSourceType:数据源类型
public enum DataSourceType {
    /**
     * 主库
     */
    MASTER,
    /**
     * 从库
     */
    SLAVE
}
  • com.shawn.spring.tool.datasource.annotation.DataSource:自定义数据源注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
    /**
     * 数据源类型,默认主库
     */
    DataSourceType value() default DataSourceType.MASTER;
}

3.AOP 实现数据源切换

  • com.shawn.spring.tool.datasource.context.DynamicDataSourceContextHolder:动态数据源上下文持有者,保存线程请求使用的数据源。
public class DynamicDataSourceContextHolder {
    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final 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 clearDataSourceType()
    {
        CONTEXT_HOLDER.remove();
    }
}
  • com.shawn.spring.tool.datasource.aspect.DataSourceAspect:数据源切面,实现数据源切换
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
    @Pointcut("@annotation(com.shawn.spring.tool.datasource.annotation.DataSource)"
            + "|| @within(com.shawn.spring.tool.datasource.annotation.DataSource)")
    public void dsPointCut() {
    }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 执行前,获取数据源保存至上下文中
        DataSource dataSource = getDataSource(point);
        if (Objects.nonNull(dataSource)) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
        }
        try {
            return point.proceed();
        } finally {
            // 执行后,清除上下文中数据源
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point)
    {
        MethodSignature signature = (MethodSignature) point.getSignature();
        //优先在方法上寻找注解
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource))
        {
            return dataSource;
        }
        //方法上没有找到,再到类上面寻找
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

4.数据源路由实现

  • com.shawn.spring.tool.datasource.routing.DynamicRoutingDataSource:动态数据源路由实现
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
  
    public DynamicRoutingDataSource(DataSource defaultTargetDataSource,
                                    Map<Object, Object> targetDataSources) {
        // 设置默认的数据源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        // 设置配置可供使用的多数据源
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
  
    @Override
    protected Object determineCurrentLookupKey() {
        // 获取当前线程上下文保存的数据源并返回
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

5.配置多数据源

  • application.yml:定义多数据源配置信息
# 数据源配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # 主库数据源
      master:
        url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: shawn
      # 从库数据源
      slave:
        # 从数据源开关
        enabled: true
        url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: shawn
      # 初始连接数
      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: ruoyi
        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
  • com.shawn.spring.tool.datasource.config.properties.DruidProperties:Druid 配置属性
@Configuration
public class DruidProperties {
    @Value("${spring.datasource.druid.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.druid.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.druid.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.druid.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.druid.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.druid.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.druid.testOnReturn}")
    private boolean testOnReturn;

    public DruidDataSource dataSource(DruidDataSource datasource) {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
        /**
         * 用来检测连接是否有效的sql。
         */
        datasource.setValidationQuery(validationQuery);
        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,
         *  如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
         * */
        datasource.setTestWhileIdle(testWhileIdle);
        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnBorrow(testOnBorrow);
        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }
}
  • com.shawn.spring.tool.datasource.config.DataSourceConfig:数据源配置
@Configuration
public class DataSourceConfig implements ApplicationContextAware {
    private ApplicationContext applicationContext;

   // 主数据源
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties properties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return properties.dataSource(dataSource);
    }
   // 从数据源
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave",
            name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }
  
  // 动态数据源路由
    @Bean(name = "dynamicRoutingDataSource")
    @Primary
    public DynamicRoutingDataSource routingDataSource(DataSource masterDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
        return new DynamicRoutingDataSource(masterDataSource, targetDataSources);
    }

    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
        try {
            DataSource dataSource = (DataSource) applicationContext.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        } catch (Exception e) {
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

四、单元测试

1.测试代码

  • com.shawn.spring.tool.datasource.User:用户信息实体
@Data
public class User {
    private Integer userId;
    private String username;
    private Integer age;
}
  • com.shawn.spring.tool.datasource.UserRepository:用户信息查询DAO
@Repository
public class UserRepository {
    @Resource
    private JdbcTemplate jdbcTemplate;
    @DataSource(DataSourceType.MASTER)
    public List<User> getAllUsers() {
        String sql = "SELECT * FROM user";
        return jdbcTemplate.query(sql, (rs, rowNum) -> {
            User user = new User();
            user.setUserId(rs.getInt("user_id"));
            user.setUsername(rs.getString("username"));
            user.setAge(rs.getInt("age"));
            return user;
        });
    }
}
  • com.shawn.spring.tool.datasource.ApiTest:单元测试类
@SpringBootTest
public class ApiTest {
    private Logger log = LoggerFactory.getLogger(ApiTest.class);
    @Resource
    private UserRepository repository;

    @Test
    public void test_getAllUsers() {
        List<User> users = repository.getAllUsers();
        log.info("用户信息:{}", JSONUtil.toJsonStr(users));
    }
}

2. 测试sql

  • 主库:master_db
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `user_id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `age` int NULL DEFAULT NULL,
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '张三', 25);

SET FOREIGN_KEY_CHECKS = 1;
  • 从库:slave_db

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `user_id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `age` int NULL DEFAULT NULL,
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '李四', 20);

SET FOREIGN_KEY_CHECKS = 1;

3. 测试结果

  • 使用 @DataSource(DataSourceType.MASTER) 注解测试结果:
  • 使用 @DataSource(DataSourceType.SLAVE) 注解测试结果:


比较两次测试结果,当使用主库,查询结果就是主库中数据,当使用从库,查询结果就是从库中数据,测试完全符合我们的预期。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表