【SpringBoot】实现动态数据源切换本文主要记录了如何在SpringBoot中实现动态数据源切换,主要步骤如下:
本文主要记录了如何在SpringBoot中实现动态数据源切换,主要步骤如下:
- 从配置文件中读取主从数据源配置
- 加载配置信息到IOC容器中
- 使用动态数据源切换[新增操作]
- 使用注解+AOP优化使用过程
前置条件:
- 开发环境:jdk8+MySQL8
- 已成功搭建 SpringBoot + MyBatis 应用。
建表的SQL
语句如下:
-- auto-generated definition
drop table user;
create table user
(
user_id int auto_increment comment '主键' primary key,
user_name varchar(20) not null comment '用户名',
password varchar(255) null
)ENGINE=InnoDB AUTO_INCREMENT=11600036 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
在配置文件中配置数据源信息
首先在配置文件中配置相关数据源的信息,这里为一主库二从库:
server:
port: 8888
spring:
master:
jdbc-url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf8
username: root
password: root123!
driver-class-name: com.mysql.cj.jdbc.Driver
slave1:
jdbc-url: jdbc:mysql://localhost:3306/slave1?useUnicode=true&characterEncoding=utf8
username: root
password: root123!
driver-class-name: com.mysql.cj.jdbc.Driver
slave2:
jdbc-url: jdbc:mysql://localhost:3306/slave2?useUnicode=true&characterEncoding=utf8
username: root
password: root123!
driver-class-name: com.mysql.cj.jdbc.Driver
加载配置信息到IOC容器中
使用如下代码加载我们的配置信息:
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource1")
@ConfigurationProperties(prefix = "spring.datasource.slave1")
public DataSource slaveDataSource1() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource2")
@ConfigurationProperties(prefix = "spring.datasource.slave2")
public DataSource slaveDataSource2() {
return DataSourceBuilder.create().build();
}
配置动态数据源
在没有使用动态数据源的时候、我们只需要配置数据库相关配置即可正常使用。了解过SpringBoot
装配原理的都知道、这是在应用启动过程中、自动装配了相关的bean
。
# AutoConfigureTestDatabase auto-configuration imports
org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase=\
org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
对应的Java代码如下:
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
// 省略其他代码
}
因此在服务启动时、就需要排除掉相关配置@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
当不使用默认配置就需要我们提供一个数据源、且是动态的。Spring
给我们提供了AbstractRoutingDataSource
这个抽象类用于实现动态的数据源。
AbstractRoutingDataSource
实现了 DataSource
接口该接口提供了如下的信息:
Abstract DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context..
翻译过来就是
抽象的 DataSource 实现。基于不同的 look key 将
getConnection
调用到目标数据源上。通常(但不一定)是通过某些线程绑定的事务上下文来确定的
在这里我们梳理一下我们已经拥有的东西:
- 我们定义了三个bean、这三个包含着具体去操作的数据库信息,
还有如下需要做的工作:
- 实现一个动态的数据源类。那么代码如下
public class MyDynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return MyDynamicDataSourceHolder.getDataSourceType();
}
}
public class MyDynamicDataSourceHolder {
public static final Logger log = LoggerFactory.getLogger(MyDynamicDataSourceHolder.class);
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dsType)
{
log.info("切换到 {} 数据源", dsType);
CONTEXT_HOLDER.set(dsType);
}
public static String getDataSourceType()
{
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType()
{
CONTEXT_HOLDER.remove();
}
}
为什么这里需要一个MyDynamicDataSourceHolder
类呢?这是为了在切换数据源时调用,就是数据源的一种映射。只要能实现切换到功能、可以使用枚举、map等都可以
接着把我们实现的动态数据源加载到IOC容器中
/**
* 创建时间: 2024年09月20日 13:05
* 文件描述: 动态数据源配置
*/
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class DataSourceConfig {
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource1")
@ConfigurationProperties(prefix = "spring.datasource.slave1")
public DataSource slaveDataSource1() {
return DataSourceBuilder.create().build();
}
@Bean(name = "slaveDataSource2")
@ConfigurationProperties(prefix = "spring.datasource.slave2")
public DataSource slaveDataSource2() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put("masterDataSource", masterDataSource());
dataSourceMap.put("slaveDataSource1", slaveDataSource1());
dataSourceMap.put("slaveDataSource2", slaveDataSource2());
MyDynamicDataSource myDynamicDataSource = new MyDynamicDataSource();
myDynamicDataSource.setDefaultTargetDataSource(masterDataSource());
myDynamicDataSource.setTargetDataSources(dataSourceMap);
return myDynamicDataSource;
}
}
到这里已经提供了动态数据源切换的能力。可以在项目中使用了。
使用动态数据源
在controller
层定义如下三个接口
@PostMapping("insert.do")
public UserDO insert(@RequestBody UserDO userDO){
log.info("user = {}",userDO);
return userService.insertUser(userDO);
}
@PostMapping("insert_slave1.do")
public UserDO insertSlave1(@RequestBody UserDO userDO){
log.info("user = {}",userDO);
return userService.insertUserBySlave1(userDO);
}
@PostMapping("insert_slave2.do")
public UserDO insertSlave2(@RequestBody UserDO userDO){
log.info("user = {}",userDO);
return userService.insertUserBySlave2(userDO);
}
代码很简单不做解释、service
层代码如下:
@Override
public UserDO insertUser(UserDO user) {
log.info("before insert User = " + user);
Integer insertRes = userMapper.insertUser(user);
if (insertRes > 0) {
log.info("after insert User = " + user);
}
return user;
}
@Override
public UserDO insertUserBySlave1(UserDO user) {
MyDynamicDataSourceHolder.setDataSourceType("slaveDataSource1");
Integer insertRes = userMapper.insertUser(user);
if (insertRes > 0) {
log.info("after insert User = " + user);
}
MyDynamicDataSourceHolder.clearDataSourceType();
return user;
}
@Override
public UserDO insertUserBySlave2(UserDO user) {
MyDynamicDataSourceHolder.setDataSourceType("slaveDataSource2");
Integer insertRes = userMapper.insertUser(user);
if (insertRes > 0) {
log.info("after insert User = " + user);
}
MyDynamicDataSourceHolder.clearDataSourceType();
return user;
}
mapper.xml
代码如下:
<insert id="insertUser" parameterType="com.mybatis.tutorial.domain.UserDO" useGeneratedKeys="true"
keyProperty="userId">
insert into user(user_name, password)
values (#{userName}, #{password})
</insert>
运行项目、访问对应的接口可以看到符合预期效果。动态数据源起作用了。
动态数据源是如何切换的
为什么上面这样写就可以实现动态数据源的切换呢?
通过
determineCurrentLookupKey()
方法会返回一个String
、这个String
就是代表的数据源bean
的beanName
,那么是在哪里通过这个beanName
加载到对应的数据源呢?
这就需要我们去看一抽象类AbstractRoutingDataSource
的代码
// 决定使用哪一个dataSource
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
// 从resolvedDataSources获取数据源。
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 + "]");
}
return dataSource;
}
// 往resolvedDataSources填充数据
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
// 获取数据库连接
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
可以看到是通过DataSource dataSource = this.resolvedDataSources.get(lookupKey);
获取了数据源、而lookupKey
正是MyDynamicDataSource.determineCurrentLookupKey()
方法的返回值。也就是在这里实现了数据源的切换。
使用注解+Aop优化使用过程
在上面的代码里、涉及到数据源切换的都需要 MyDynamicDataSourceHolder.setDataSourceType("beanName");
因此我们可以通过AOP来优化这一行代码。
如何切换到想要的数据源呢?则可以通过注解实现。注解代码如下:
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {
String value() default "masterDataSource";
}
环绕通知代码如下:
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.mybatis.tutorial.config.datasource.DynamicDataSource)")
private void dynamicDataSource(){
}
@Around("dynamicDataSource()")
public Object around(ProceedingJoinPoint point) throws Throwable{
MethodSignature signature = (MethodSignature) point.getSignature();
DynamicDataSource dynamicDataSource = signature.getMethod().getAnnotation(DynamicDataSource.class);
if(dynamicDataSource != null){
MyDynamicDataSourceHolder.setDataSourceType(dynamicDataSource.value());
}else {
MyDynamicDataSourceHolder.setDataSourceType("masterDataSource");
}
try {
return point.proceed();
}finally {
MyDynamicDataSourceHolder.clearDataSourceType();
}
}
}
测试代码、和预期一致。到此所有功能就已经完全实现了。
可以继续优化的地方:
- 配置文件的读取可以写一个
DataSourceProperties
来接收配置值。 - 可以考虑将这一个动态数据源封装为一个
starter
、这样在后续的项目过程中使用到了此功能就可以直接引入。
转载自:https://juejin.cn/post/7416292550754500658