动态切换数据源的最佳实践

序言

本文和大家聊聊在开发中,动态切换多数据源的方案。

一、多数据源需求

随着应用程序的发展和复杂性增加,对于多数据源的需求也变得越来越普遍。在某些场景下,一个应用程序可能需要连接和操作多个不同的数据库或数据源。常见的场景包括多租户系统、分布式架构、数据分片、读写分离以及数据同步和迁移等。在这些场景下,应用程序需要连接到多个数据源来满足不同的业务需求。

二、动态切换多数据源设计

在设计动态切换数据源的方案时,需要考虑以下几个方面:

  1. 数据源的管理和配置:如何管理和配置多个数据源,以便应用程序能够动态地切换数据源。
  2. 数据源的路由和选择:如何根据业务需求选择合适的数据源,并在运行时动态切换数据源。
  3. 数据源的连接池管理:如何有效地管理多个数据源的连接池,以提高系统的性能和资源利用率。

三、动态切换多数据源关键技术

实现动态切换数据源的关键技术包括:

  1. 使用 Spring 框架的 AbstractRoutingDataSource 实现动态数据源路由。
  2. 使用 AOP + 注解方式拦截数据源访问方法,并在运行时动态切换数据源。

四、动态切换多数据源核心原理

在 Spring 中提供了一个 AbstractRoutingDataSource 抽象类,用于实现动态路由到不同数据源的功能。它允许应用程序根据特定的规则在运行时选择使用哪个数据源,而不是在启动时就确定使用哪个数据源。

其原理如下:

workspace.png

  1. 开发人员将多个 DataSource(数据源)对象放入 AbstractRoutingDataSource 的 targetDataSources 成员变量中。其中,targetDataSources 是一个 Map<Object, Object> 集合,key 存放的是 DataSource 的名称,value 存放具体 DataSource 对象。
  2. 开发人员实现 AbstractRoutingDataSource#determineCurrentLookupKey() 方法,该方法返回 DataSource 的 key。
  3. AbstractRoutingDataSource 会根据 AbstractRoutingDataSource#determineCurrentLookupKey() 返回的 key 查找相应的 DataSource 对象,从而实现了动态指定数据源

五、实现方案

5.1 数据源管理和配置

首先,我们需要定义多数据源的配置方式以及管理方式。我们在 spring.datasource 的基础上,添加一个 multi 属性定义多数据源。其中,multi 下是数据源列表,具体格式如下:

spring:
  datasource:
    multi:
      - name: master
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.56.101:3306/learn
        username: root
        password: 123456
        type: com.alibaba.druid.pool.DruidDataSource

      - name: slave
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.56.102:3306/test
        username: root
        password: 123456
        type: com.alibaba.druid.pool.DruidDataSource

读取自定义配置属性的配置类:

@ConfigurationProperties(prefix = "spring.datasource")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MultiDataSourceProperties {
    // DataSourceProperties 是 Spring 里面的
    private List<DataSourceProperties> multi;
}

5.2 数据源动态路由规则

实现动态路由切换数据源的关键是在 AbstractRoutingDataSource#determineCurrentLookupKey() 方法里,因为 AbstractRoutingDataSource 会根据其返回的 key 去查找相应的 DataSource。
workspace (1).png

我们可以将路由规则进行如下处理:

  1. determineCurrentLookupKey() 直接从 ThreadLocal 中获取 DataSource 的 key 返回
  2. 开发者动态更换 ThreadLocal 中的值,即可实现动态路由

定义 ThreadLocal 的操作对象(实现对 ThreadLocal 的操作):

public class DataSourceContextHolder {

    public static final ThreadLocal<String> DATASOURCE_CONTEXT_HOLDER = new ThreadLocal<>();

    // 放入 DataSource 的 key
    public static void setDataSourceContext(String dataSource) {
        DATASOURCE_CONTEXT_HOLDER.set(dataSource);
    }

    // 获取 DataSource 的 key
    public static String getDataSource() {
        return DATASOURCE_CONTEXT_HOLDER.get();
    }

    // 清除 DataSource 的 key
    public static void clear() {
        DATASOURCE_CONTEXT_HOLDER.remove();
    }
}

根据配置生成数据源,并实现路由规则:

@Configuration
@EnableConfigurationProperties({MultiDataSourceProperties.class})
public class DynamicDataSourceAutoConfigure {

    private final MultiDataSourceProperties multiDataSourceProperties;
    private final TreeMap<Object, Object> targetDataSources = new TreeMap<>();

    /**
     * 构造器注入
     *
     * @param multiDataSourceProperties 数据源配置
     */
    @Autowired
    public DynamicDataSourceAutoConfigure(MultiDataSourceProperties multiDataSourceProperties) {
        this.multiDataSourceProperties = multiDataSourceProperties;
    }

    /**
     * 该方法根据数据源配置生成对应的 DataSource 对象
     *
     * @param dataSourceProperties 数据源配置
     * @return DataSource
     */
    private DataSource createDataSource(DataSourceProperties dataSourceProperties) {
        return DataSourceBuilder.create()
                .driverClassName(dataSourceProperties.getDriverClassName())
                .url(dataSourceProperties.getUrl())
                .username(dataSourceProperties.getUsername())
                .password(dataSourceProperties.getPassword())
                .type(dataSourceProperties.getType())
                .build();
    }

    /**
     *
     * 在实例化时根据配置动态的创建多个数据源
     */
    @PostConstruct
    public void init() {
        List<DataSourceProperties> dataSources = multiDataSourceProperties.getMulti();
        for (DataSourceProperties dataSourceProperties : dataSources) {
            // 创建数据源
            DataSource dataSource = createDataSource(dataSourceProperties);
            // 将数据源放入 targetDataSources
            targetDataSources.put(dataSourceProperties.getName(), dataSource);
        }
    }

    /**
     * 注入自定义的 AbstractRoutingDataSource,并实现路由规则
     *
     * @return DataSource
     */
    @Bean
    public DataSource dynamicDataSource() {
        AbstractRoutingDataSource dataSource = new AbstractRoutingDataSource() {
            
            @Override
            protected Object determineCurrentLookupKey() {
                // 路由规则:直接从 ThreadLocal 获取 DataSource 的 key
                return DataSourceContextHolder.getDataSource();
            }
        };

        // 设置默认数据源为配置文件的第一个数据源
        dataSource.setDefaultTargetDataSource(targetDataSources.firstEntry().getValue());
        // 配置数据源列表
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }
}

5.3 动态切换数据源

之前,数据源的动态路由规则已经定义完成了。但是这个规则是依据 ThreadLocal 中值的动态变化完成的。如何动态设置 ThreadLocal 中的值就成了关键。动态设置 ThreadLocal 中的值其实并不难,为了使我们的开发更加方便,我们采用 AOP + 注解 的方式,从而实现声明式动态更改 ThreadLocal 中的值。

  1. 定义一个注解

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DS {
        String value() default "";
    }
    
  2. 给该注解添加 AOP 处理逻辑

    @Aspect
    @Component
    public class DynamicDataSourceAspect {
    
        // 可在类和方法上检测该注解
        @Before("@annotation(dataSource) || @within(dataSource)")
        public void before(JoinPoint joinPoint, DS dataSource) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            DS annotation = method.getAnnotation(DS.class);
    
            String value = annotation != null ? annotation.value() : dataSource.value();
    
            // 将注解中的值放入 ThreadLocal 中
            DataSourceContextHolder.setDataSourceContext(value);
        }
    
        @After("@annotation(dataSource) || @within(dataSource)")
        public void after(DS dataSource) {
            // 清除 ThreadLocal 中的值
            DataSourceContextHolder.clear();
        }
    }
    
  3. 使用方式

    @Service
    public class UserServiceImpl implements UserService {
    
        @Resource
        private UserMapper userMapper;
    
        // 使用在方法上
        @DS("slave")
        @Override
        public User getUser(int userId) {
            return userMapper.getUserById(userId);
        }
    }
    
    
    // 使用在类上
    @DS("slave")
    @Service
    public class UserServiceImpl implements UserService {
    
        @Resource
        private UserMapper userMapper;
    
    
        @Override
        public User getUser(int userId) {
            return userMapper.getUserById(userId);
        }
    }
    

至此,我们便完成了多数据源的动态切换。今后我们若有需要只需:

  1. 在配置文件中添加数据源配置
  2. 使用 @DS 注解就可以完成数据源的切换了。

六、FAQ

本文主要是提供动态切换数据源的核心思路,若大家有特殊开发需求可以自行借助搜索引擎或在评论区下大家一起讨论哦。(‾◡◝)

推荐阅读

  1. 缓存神器-JetCache
  2. Mybatis 缓存机制
  3. 为什么 MySQL 单表数据量最好别超过 2000w
  4. IoC 思想简单而深邃
  5. ThreadLocal
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值