序言
本文和大家聊聊在开发中,动态切换多数据源的方案。
一、多数据源需求
随着应用程序的发展和复杂性增加,对于多数据源的需求也变得越来越普遍。在某些场景下,一个应用程序可能需要连接和操作多个不同的数据库或数据源。常见的场景包括多租户系统、分布式架构、数据分片、读写分离以及数据同步和迁移等。在这些场景下,应用程序需要连接到多个数据源来满足不同的业务需求。
二、动态切换多数据源设计
在设计动态切换数据源的方案时,需要考虑以下几个方面:
- 数据源的管理和配置:如何管理和配置多个数据源,以便应用程序能够动态地切换数据源。
- 数据源的路由和选择:如何根据业务需求选择合适的数据源,并在运行时动态切换数据源。
- 数据源的连接池管理:如何有效地管理多个数据源的连接池,以提高系统的性能和资源利用率。
三、动态切换多数据源关键技术
实现动态切换数据源的关键技术包括:
- 使用 Spring 框架的
AbstractRoutingDataSource
实现动态数据源路由。 - 使用
AOP + 注解
方式拦截数据源访问方法,并在运行时动态切换数据源。
四、动态切换多数据源核心原理
在 Spring 中提供了一个 AbstractRoutingDataSource
抽象类,用于实现动态路由到不同数据源的功能。它允许应用程序根据特定的规则在运行时选择使用哪个数据源,而不是在启动时就确定使用哪个数据源。
其原理如下:
- 开发人员将多个
DataSource
(数据源)对象放入 AbstractRoutingDataSource 的targetDataSources
成员变量中。其中,targetDataSources 是一个Map<Object, Object>
集合,key 存放的是 DataSource 的名称,value 存放具体 DataSource 对象。 - 开发人员实现
AbstractRoutingDataSource#determineCurrentLookupKey()
方法,该方法返回 DataSource 的 key。 - AbstractRoutingDataSource 会根据
AbstractRoutingDataSource#determineCurrentLookupKey()
返回的 key 查找相应的 DataSource 对象,从而实现了动态指定数据源
五、实现方案
5.1 数据源管理和配置
首先,我们需要定义多数据源的配置方式以及管理方式。我们在 spring.datasource
的基础上,添加一个 multi
属性定义多数据源。其中,multi 下是数据源列表,具体格式如下:
spring:datasource:multi:- name: masterdriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.56.101:3306/learnusername: rootpassword: 123456type: com.alibaba.druid.pool.DruidDataSource- name: slavedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.56.102:3306/testusername: rootpassword: 123456type: 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。
我们可以将路由规则进行如下处理:
- determineCurrentLookupKey() 直接从 ThreadLocal 中获取 DataSource 的 key 返回
- 开发者动态更换 ThreadLocal 中的值,即可实现动态路由
定义 ThreadLocal 的操作对象(实现对 ThreadLocal 的操作):
public class DataSourceContextHolder {public static final ThreadLocal<String> DATASOURCE_CONTEXT_HOLDER = new ThreadLocal<>();// 放入 DataSource 的 keypublic static void setDataSourceContext(String dataSource) {DATASOURCE_CONTEXT_HOLDER.set(dataSource);}// 获取 DataSource 的 keypublic static String getDataSource() {return DATASOURCE_CONTEXT_HOLDER.get();}// 清除 DataSource 的 keypublic 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 数据源配置*/@Autowiredpublic 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();}/**** 在实例化时根据配置动态的创建多个数据源*/@PostConstructpublic void init() {List<DataSourceProperties> dataSources = multiDataSourceProperties.getMulti();for (DataSourceProperties dataSourceProperties : dataSources) {// 创建数据源DataSource dataSource = createDataSource(dataSourceProperties);// 将数据源放入 targetDataSourcestargetDataSources.put(dataSourceProperties.getName(), dataSource);}}/*** 注入自定义的 AbstractRoutingDataSource,并实现路由规则** @return DataSource*/@Beanpublic DataSource dynamicDataSource() {AbstractRoutingDataSource dataSource = new AbstractRoutingDataSource() {@Overrideprotected Object determineCurrentLookupKey() {// 路由规则:直接从 ThreadLocal 获取 DataSource 的 keyreturn DataSourceContextHolder.getDataSource();}};// 设置默认数据源为配置文件的第一个数据源dataSource.setDefaultTargetDataSource(targetDataSources.firstEntry().getValue());// 配置数据源列表dataSource.setTargetDataSources(targetDataSources);return dataSource;}
}
5.3 动态切换数据源
之前,数据源的动态路由规则已经定义完成了。但是这个规则是依据 ThreadLocal 中值的动态变化完成的。如何动态设置 ThreadLocal 中的值就成了关键。动态设置 ThreadLocal 中的值其实并不难,为了使我们的开发更加方便,我们采用 AOP + 注解
的方式,从而实现声明式动态更改 ThreadLocal 中的值。
-
定义一个注解
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DS {String value() default ""; }
-
给该注解添加 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();} }
-
使用方式
@Service public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;// 使用在方法上@DS("slave")@Overridepublic User getUser(int userId) {return userMapper.getUserById(userId);} }// 使用在类上 @DS("slave") @Service public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Overridepublic User getUser(int userId) {return userMapper.getUserById(userId);} }
至此,我们便完成了多数据源的动态切换。今后我们若有需要只需:
- 在配置文件中添加数据源配置
- 使用
@DS
注解就可以完成数据源的切换了。
六、FAQ
本文主要是提供动态切换数据源的核心思路,若大家有特殊开发需求可以自行借助搜索引擎或在评论区下大家一起讨论哦。(‾◡◝)
推荐阅读
- 缓存神器-JetCache
- Mybatis 缓存机制
- 为什么 MySQL 单表数据量最好别超过 2000w
- IoC 思想简单而深邃
- ThreadLocal