Springcloud学习笔记57--Spring 多数据源切换
1 环境准备
pom文件
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.9.RELEASE</version> <relativePath/> </parent> <dependencies> <!-- druid数据源驱动 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--mybatis SpringBoot依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!-- aop依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.1</version> </dependency> <!-- 通用mapper --> <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>1.1.5</version> </dependency> <!-- druid监控依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.28</version> </dependency> </dependencies>
2. DynamicDataSource 动态数据源
在spring中有一个抽象类AbstractRoutingDataSource类,通过这个类可以实现动态数据源切换。如下是这个类的成员变量
private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource; private Map<Object, DataSource> resolvedDataSources;
- targetDataSources保存了key和数据库连接的映射关系
- defaultTargetDataSource标识默认的连接
- resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储结构也是数据库标识和数据源的映射关系
自定义DynamicDataSource,继承AbstractRoutingDataSource类
package com.ttbank.flep.util; import com.zaxxer.hikari.HikariDataSource; import lombok.extern.slf4j.Slf4j; import org.springframework.core.env.Environment; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import javax.annotation.Resource; import javax.sql.DataSource; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @Author lucky * @Date 2023/9/20 9:49 */ @Slf4j public class DynamicDataSource extends AbstractRoutingDataSource { private ConcurrentHashMap<Object,Object> targetDataSources; @Resource private Environment env; @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.getDataSource(); } public synchronized void setTargetDataSources(Map<Object,Object> targetDataSources){ ConcurrentHashMap<Object,Object> map=new ConcurrentHashMap<>(); map.putAll(targetDataSources); this.targetDataSources=map; super.setTargetDataSources(map); super.afterPropertiesSet(); } public synchronized void addDataSource(String dataSourceKey,String driverClassName,String url,String username,String password){ if(dataSourceKey==null|| "".equals(dataSourceKey)){ log.warn("dataSourceKey is empty"); } DataSourceUsedHolder.use(); Object value = this.targetDataSources.get(dataSourceKey); if(value==null){ value=this.targetDataSources.get(dataSourceKey); if(value==null){ DataSource dataSource = this.createDataSource(driverClassName, url, username, password); this.targetDataSources.put(dataSourceKey,dataSource ); this.setTargetDataSources(this.targetDataSources); log.info("add DataSource named [{}]",dataSourceKey); return; } } log.info("DataSource named [{]] already exists,ignore to add",dataSourceKey); } private DataSource createDataSource(String driverClassName,String url,String username,String password){ HikariDataSource ds=new HikariDataSource(); try{ ds.setJdbcUrl(url); ds.setUsername(username); ds.setPassword(password); ds.setDriverClassName(driverClassName); }catch (Exception e){ e.printStackTrace(); } return ds; } }
AbstractRoutingDataSource实现了InitializingBean接口,并实现了afterPropertiesSet方法。afterPropertiesSet方法是初始化bean的时候执行,可以针对某个具体的bean进行执行;
下面是AbstractRoutingDataSource类中afterPropertiesSet方法的关键源码;
@Override public void afterPropertiesSet() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); } this.resolvedDataSources = new HashMap<Object, DataSource> (this.targetDataSources.size()); //初始化resolvedDataSources //循环targetDataSources,并添加到resolvedDataSources中 for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) { Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); this.resolvedDataSources.put(lookupKey, dataSource); } //如果默认数据源不为空则指定对应数据源 if (this.defaultTargetDataSource != null) { this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource); } }
这里的targetDataSources属性(map)是存储将要切换的多数据源bean信息。
而如果数据源在数据库中,则需要重写方法determineCurrentLookupKey(),因为数据源bean是动态生成的,然后需要添加到targetDataSources中,此时需要调用afterPropertiesSet()方法,来通知spring有bean更新。
因为此抽象类中都是引用resolvedDataSources属性,所以在此方法中将targetDataSources属性的键值信息存储到resolvedDataSources属性中,以便后续调用。
最后,看一下AbstractRoutingDataSource类中的连接数据库的getConnection()方法,调用的是determineTargetDataSource()方法,来创建连接。
@Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); }
而determineTargetDataSource()方法是决定spring容器连接那个数据源
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); //具体选择哪个数据源 Object lookupKey = determineCurrentLookupKey(); 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; }
而选择哪个数据源又是由determineCurrentLookupKey()方法来决定的,此方法是抽象方法,需要我们继承AbstractRoutingDataSource抽象类来重写此方法。该方法返回一个key,该key是bean中的beanName,并赋值给lookupKey,由此key可以通过resolvedDataSources属性的键来获取对应的DataSource值,从而达到数据源切换的功能。
3.利用ThreadLocal实现数据源线程隔离
定义DynamicDataSourceHolder
package com.ttbank.flep.util; /** * @Author lucky * @Date 2023/9/20 9:43 */ public class DynamicDataSourceHolder { public static final ThreadLocal<String> holder=new ThreadLocal<>(); public DynamicDataSourceHolder() { } public static synchronized void setDataSource(String name){ holder.set(name); DataSourceUsedHolder.use(); } public static synchronized String getDataSource(){ return holder.get(); } public static void clearDataSource(){ holder.remove(); } }
定义DataSourceUsedHolder
package com.ttbank.flep.util; /** * @Author lucky * @Date 2023/9/20 9:38 */ public class DataSourceUsedHolder { public static final ThreadLocal<Boolean> holder=new ThreadLocal<>(); public DataSourceUsedHolder() { } public static void use(){ holder.set(true); } public static Boolean IsUsed(){ return holder.get(); } public static void reSet(){ holder.remove(); } }
注意:每个接口或线程使用完数据源后,需要将数据源清空,否则会出现多线程情况下数据源切换混乱的情况;建议添加以下代码:
DynamicDataSourceHolder.clearDataSource();
4 定义数据源操作接口
用于统一添加数据源;
public interface DataSourceService { String addDataSource(DataSourceInput dataSourceInput); }
package com.ttbank.flep.service; import com.ttbank.flep.util.DataSourceInput; import com.ttbank.flep.util.DynamicDataSource; import javax.annotation.Resource; import javax.sql.DataSource; /** * @Author lucky * @Date 2023/9/20 10:27 */ public class DataSourceServiceImpl implements DataSourceService { @Resource private DataSource dataSource; @Override public String addDataSource(DataSourceInput input) { ((DynamicDataSource)this.dataSource) .addDataSource(input.getDataSourceKey(),input.getDriverClassName() ,input.getUrl() ,input.getUsername() ,input.getPassword() ); return "success"; } }
5 切换数据源工具类封装
DpDataSourceProperties用于读取yml文件中的配置;
@Component @ConfigurationProperties(prefix = "spring.datasource.dp") @Data public class DpDataSourceProperties { private String dataSourceKey; private String driverClassName; private String url; private String username; private String password; }
数据源传参数DTO
@Data @Builder @AllArgsConstructor @NoArgsConstructor public class DataSourceInput { private String dataSourceKey; private String driverClassName; private String url; private String username; private String password; }
数据源切换工具类:
package com.ttbank.flep.util; import com.ttbank.flep.pojo.DpDataSourceProperties; import com.ttbank.flep.service.DataSourceService; import org.apache.commons.lang.StringUtils; /** * @Author lucky * @Date 2023/9/20 10:32 */ public class DataSourceChangeUtil { public static DataSourceInput generateDataSource(String dataSourceKey,String driverClassName,String url,String username,String password){ DataSourceInput input=new DataSourceInput(); input.setDataSourceKey(dataSourceKey); if(StringUtils.isEmpty(dataSourceKey)){ driverClassName="com.mysql.jdbc.Driver"; } input.setDriverClassName(driverClassName); input.setUrl(url); input.setUsername(username); input.setPassword(password); return input; } public static void changeDpDataSoource(DpDataSourceProperties dpDataSourceProperties, DataSourceService dataSourceService){ DynamicDataSourceHolder.clearDataSource(); DataSourceInput dataSourceInput = generateDataSource(dpDataSourceProperties.getDataSourceKey(), dpDataSourceProperties.getDriverClassName(), dpDataSourceProperties.getUrl(), dpDataSourceProperties.getUsername(), dpDataSourceProperties.getPassword()); dataSourceService.addDataSource(dataSourceInput); //切换数据源 DynamicDataSourceHolder.setDataSource("dp"); } }
参考文献:
https://blog.csdn.net/syslbjjly/article/details/93492535(推荐)
https://blog.csdn.net/qq_44665283/article/details/129237053
https://blog.csdn.net/qq_32037561/article/details/126082651(推荐)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
2022-09-14 RocketMQ 01---RocketMQ 介绍及基本概念
2021-09-14 Java基础知识11--Optional类