动态切换数据源的最佳实践
动态切换数据源的最佳实践
在现代应用程序开发中,多数据源的需求越来越普遍。无论是多租户系统、分布式架构还是读写分离场景,都需要能够动态切换数据源的能力。本文将详细介绍如何在Spring框架下实现动态切换多数据源的方案,包括数据源管理、路由规则设计以及具体的实现代码。
一、多数据源需求
随着应用程序的发展和复杂性增加,对于多数据源的需求也变得越来越普遍。在某些场景下,一个应用程序可能需要连接和操作多个不同的数据库或数据源。常见的场景包括多租户系统、分布式架构、数据分片、读写分离以及数据同步和迁移等。在这些场景下,应用程序需要连接到多个数据源来满足不同的业务需求。
二、动态切换多数据源设计
在设计动态切换数据源的方案时,需要考虑以下几个方面:
- 数据源的管理和配置:如何管理和配置多个数据源,以便应用程序能够动态地切换数据源。
- 数据源的路由和选择:如何根据业务需求选择合适的数据源,并在运行时动态切换数据源。
- 数据源的连接池管理:如何有效地管理多个数据源的连接池,以提高系统的性能和资源利用率。
三、动态切换多数据源关键技术
实现动态切换数据源的关键技术包括:
- 使用 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: 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。
我们可以将路由规则进行如下处理:
determineCurrentLookupKey()
直接从 ThreadLocal 中获取 DataSource 的 key 返回- 开发者动态更换 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 中的值。
- 定义一个注解
@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 {
@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);
}
}
至此,我们便完成了多数据源的动态切换。今后我们若有需要只需:
- 在配置文件中添加数据源配置
- 使用
@DS
注解就可以完成数据源的切换了。