当前位置:网站首页>动态切换数据源
动态切换数据源
2022-07-06 12:28:00 【小开心】
思路
- 自定义一个注解 @DataSource,将来可以将该注解加在 service 层方法或者类上面,表示方法或者类中的所有方法都使用某一个数据源。
- 对于第一步,如果某个方法上面有 @DataSource 注解,那么就将该方法需要使用的数据源名称存入到 ThreadLocal。
- 自定义切面,在切面中解析 @DataSource 注解,当一个方法或者类上面有 @DataSource 注解的时候,将 @DataSource 注解所标记的数据源存入到 ThreadLocal 中。
- 最后,当 Mapper 执行的时候,需要 DataSource,他会自动去 AbstractRoutingDataSource 类中查找需要的数据源,我们只需要在 AbstractRoutingDataSource 中返回 ThreadLocal 中的值即可。
项目代码链接:https://github.com/1040580896/dynamic_datasourece
1. 预备知识
想要自定义动态数据源切换,得先了解一个类 AbstractRoutingDataSource:
AbstractRoutingDataSource 是在 Spring2.0.1 中引入的(注意是 Spring2.0.1 不是 Spring Boot2.0.1,所以这其实也算是 Spring 一个非常古老的特性了), 该类充当了 DataSource 的路由中介,它能够在运行时, 根据某种 key 值来动态切换到真正的 DataSource 上。
大致的用法就是你提前准备好各种数据源,存入到一个 Map 中,Map 的 key 就是这个数据源的名字,Map 的 value 就是这个具体的数据源,然后再把这个 Map 配置到 AbstractRoutingDataSource 中,最后,每次执行数据库查询的时候,拿一个 key 出来,AbstractRoutingDataSource 会找到具体的数据源去执行这次数据库操作。
2、引入依赖
首先我们创建一个 Spring Boot 项目,引入 Web、MyBatis 以及 MySQL 依赖,项目创建成功之后,再手动加入 Druid 和 AOP 依赖,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
3、配置文件
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
ds:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
username: root
password: th123456
# 从库数据源
slave:
url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
username: root
password: th123456
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: tienchin
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: tru
都是 Druid 的常规配置,也没啥好说的,唯一需要注意的是我们整个配置文件的格式。ds 里边配置我们的所有数据源,每个数据源都有一个名字,master 是默认数据源的名字,不可修改,其他数据源都可以自定义名字。最后面我们还配置了 Druid 的监控功能,如果小伙伴们还不懂 Druid 的监控功能,可以查看Spring Boot 如何监控 SQL 运行情况?。
4、自定义注解和切面
注解
这个注解将来加在 Service 层的方法上,使用该注解的时候,需要指定一个数据源名称,不指定的话,默认就使用 master 作为数据源。
/** * 将来可以加在某一个service 类上或者方法上,通过value属性来指定类或者方法应该使用哪一个数据源 */
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.TYPE,ElementType.METHOD})
public @interface DataSource {
/** * 如果一个方法上加了 @DataSource 但是却没有指定数据源名称,那么默认使用 master 数据源 * @return */
String value() default DataSourceType.DEFAULT_DS_NAME;
}
切面
过 AOP 来解析当前的自定义注解
这里使用了ThreadLocal来保存需要哪种数据源
MethodSignature signature = (MethodSignature) pjb.getSignature();
AnnotationUtils.findAnnotation工具类
ThreadLocal 的特点,简单说就是在哪个线程中存入的数据,在哪个线程才能取出来,换一个线程就取不出来了,这样可以确保多线程环境下的数据安全。
@Component
@Aspect
public class DataSourceAspect {
/** * 切点 * * @annotation(com.th.annotation.DataSource 表示方法上有 @DataSource 注解就将方法拦截下来 * @within(com.th.annotation.DataSource) 表示如果类上面有 @DataSource 注解,就将类中的方法拦截下来 */
@Pointcut("@annotation(com.th.annotation.DataSource) || @within(com.th.annotation.DataSource)")
public void pc() {
}
@Around("pc()")
public Object around(ProceedingJoinPoint pjb) {
//获取方法上面的注解
DataSource dataSource = getDataSourece(pjb);
if (dataSource != null) {
//数据源的名称
String value = dataSource.value();
DynmaicDataSourceContextHolder.setDataSourceType(value);
}
try {
return pjb.proceed();
} catch (Throwable e) {
e.printStackTrace();
} finally {
DynmaicDataSourceContextHolder.clearDataSourceType();
}
return null;
}
private DataSource getDataSourece(ProceedingJoinPoint pjb) {
MethodSignature signature = (MethodSignature) pjb.getSignature();
//查找方法上面的注解
DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (annotation != null) {
//说明方法上面有 DataSource 注解
return annotation;
}
//类上面找
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}
总结
- 首先,我们在 dsPc() 方法上定义了切点,我们拦截下所有带有
@DataSource注解的方法,同时由于该注解也可以加在类上,如果该注解加在类上,就表示类中的所有方法都使用该数据源。 - 接下来我们定义了一个环绕通知,首先根据当前的切点,调用 getDataSource 方法获取到
@DataSource注解,这个注解可能来自方法上也可能来自类上,方法上的优先级高于类上的优先级。如果拿到的注解不为空,则我们在 DynamicDataSourceContextHolder 中设置当前的数据源名称,设置完成后进行方法的调用;如果拿到的注解为空,那么就直接进行方法的调用,不再设置数据源了(将来会自动使用默认的数据源)。最后记得方法调用完成后,从 ThreadLocal 中移除数据源。
4、动态获取配置
@ConfigurationProperties(prefix = "spring.datasource"),注意结构,选择合适的类型存储
package com.th.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.sql.DataSource;
import java.util.Map;
/** * @program: dynamic_datasourece * @description: * @author: xiaokaixin * @create: 2022-05-22 15:45 **/
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
private String type;
private String driverClassName;
private Map<String,Map<String,String>> ds;
private Integer initialSize;
private Integer minIdle;
private Integer maxActive;
private Integer maxWait;
/** * 在外部构造好一个 DruidDataSource 对象,但是这个对象只包含三个核心属性 url,username,passowd * 在这个方法中,给这个对象设置公共属性 * @param druidDataSource * @return */
public DataSource dataSource(DruidDataSource druidDataSource){
druidDataSource.setInitialSize(initialSize);
druidDataSource.setMaxActive(maxActive);
druidDataSource.setMinIdle(minIdle);
druidDataSource.setMaxWait(maxWait);
//。。。。。
return druidDataSource;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public Map<String, Map<String, String>> getDs() {
return ds;
}
public void setDs(Map<String, Map<String, String>> ds) {
this.ds = ds;
}
public Integer getInitialSize() {
return initialSize;
}
public void setInitialSize(Integer initialSize) {
this.initialSize = initialSize;
}
public Integer getMinIdle() {
return minIdle;
}
public void setMinIdle(Integer minIdle) {
this.minIdle = minIdle;
}
public Integer getMaxActive() {
return maxActive;
}
public void setMaxActive(Integer maxActive) {
this.maxActive = maxActive;
}
public Integer getMaxWait() {
return maxWait;
}
public void setMaxWait(Integer maxWait) {
this.maxWait = maxWait;
}
}
5、存储数据源名称
对于当前数据库操作使用哪个数据源?我们有很多种不同的设置方案,当然最为省事的办法是把当前使用的数据源信息存入到 ThreadLocal 中,ThreadLocal 的特点,简单说就是在哪个线程中存入的数据,在哪个线程才能取出来,换一个线程就取不出来了,这样可以确保多线程环境下的数据安全。
ThreadLocal(DynmaicDataSourceContextHolder)
/** * 这个类用来存储当前线程所使用的数据源名称 */
public class DynmaicDataSourceContextHolder {
private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dsType){
CONTEXT_HOLDER.set(dsType);
}
public static String getDataSourceType(){
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType(){
CONTEXT_HOLDER.remove();
}
}
6、加载数据源
@EnableConfigurationProperties(DruidProperties.class)
先说作用:
@EnableConfigurationProperties注解的作用是:使使用 @ConfigurationProperties 注解的类生效。
说明:
如果一个配置类只配置@ConfigurationProperties注解,而没有使用@Component,那么在IOC容器中是获取不到properties 配置文件转化的bean。说白了 @EnableConfigurationProperties 相当于把使用 @ConfigurationProperties 的类进行了一次注入。
测试发现 @ConfigurationProperties 与 @EnableConfigurationProperties 关系特别大
@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource {
@Autowired
DruidProperties druidProperties;
public Map<String, DataSource> loadAllDataSource(){
Map<String,DataSource> map = new HashMap<>();
Map<String, Map<String, String>> ds = druidProperties.getDs();
try {
Set<String> keySet = ds.keySet();
for (String key : keySet) {
//druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))) 公共属性也设置上去了
map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
7、数据源切换
这就是我们文章开头所说的 AbstractRoutingDataSource 了,该类有一个方法名为 determineCurrentLookupKey,当需要使用数据源的时候,系统会自动调用该方法,获取当前数据源的标记,如 master 或者 slave 或者其他,拿到标记之后,就可以据此获取到一个数据源了。
当我们配置 DynamicDataSource 的时候,需要配置两个关键的参数,一个是 setTargetDataSources,这个就是当前所有的数据源,把当前所有的数据源都告诉给 AbstractRoutingDataSource,这些数据源都是 key-value 的形式(将来根据 determineCurrentLookupKey 方法返回的 key 就可以获取到具体的数据源了);另一个方法是 setDefaultTargetDataSource,这个就是默认的数据源,当我们执行一个数据库操作的时候,如果没有指定数据源(例如 Service 层的方法没有加 @DataSource 注解),那么默认就使用这个数据源。
最后,再将这个 bean 注册到 Spring 容器中,如下:
@Component
public class DynmaicDataSource extends AbstractRoutingDataSource {
public DynmaicDataSource(LoadDataSource loadDataSource) {
//1、设置所有的数据源
Map<String, DataSource> allDs = loadDataSource.loadAllDataSource();
super.setTargetDataSources(new HashMap<>(allDs));
//2、设置默认的数据源
//将来,并不是所以方法上都有 @DataSourece注解 对于哪些没有 @DataSouce 注解的方法,该使用那个数据源?
super.setDefaultTargetDataSource(allDs.get(DataSourceType.DEFAULT_DS_NAME));
//3
super.afterPropertiesSet();
}
/** * 用来返回数据源名称,当系统需要数据源的时候,会自动调用该方法获取数据源的名称 * @return */
@Override
protected Object determineCurrentLookupKey() {
return DynmaicDataSourceContextHolder.getDataSourceType();
}
}
7、测试
好啦,大功告成,我们再来测试一下,写一个 UserMapper:
@Mapper
public interface UserMapper {
@Select("select * from user")
List<User> getAllUsers();
}
通过 @DataSource 注解来指定具体操作的数据源,如果没有使用该注解指定,默认就使用 master 数据源。
最后去单元测试中测一下,如下:
再来一个 service:
@Service
@DataSource("slave")
public class UserService {
@Autowired
UserMapper userMapper;
//@DataSource("master")
public List<User> getAllUsers(){
return userMapper.getAllUsers();
}
}
基于页面手动切换数据源
aop
@Order(10)越小,后执行的aop会覆盖之前的数据源
@Aspect
@Component
@Order(10)
public class GloalbDataSourceAspetc {
@Autowired
HttpSession session;
@Pointcut("execution(* com.th.service.*.*(..))")
public void pc(){
}
@Around("pc()")
public Object around(ProceedingJoinPoint pjb){
DynmaicDataSourceContextHolder.setDataSourceType((String) session.getAttribute(DataSourceType.DS_SESSION_KEY));
try {
return pjb.proceed();
} catch (Throwable e) {
e.printStackTrace();
}finally {
DynmaicDataSourceContextHolder.clearDataSourceType();
}
return null;
}
}
HttpSession session,也可以用其他方式存储
@RestController
public class DataSourceController {
private static final Logger log = LoggerFactory.getLogger(DataSourceController.class);
@Autowired
UserService userService;
/** * 修改数据源的接口 */
@PostMapping("/dstype")
public void setDsType(String dsType, HttpSession session){
//将数据源的信息存放到session
session.setAttribute(DataSourceType.DS_SESSION_KEY,dsType);
log.info("数据源切换为:{}",dsType);
}
@GetMapping("/users")
public List<User> getAllUsers(){
return userService.getAllUsers();
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="jquery.js"></script>
</head>
<body>
<div>
请选择数据源
<select name="" id="" onchange="dfChange(this.options[this.options.selectedIndex].value)">
<option value="请选择">请选择</option>
<option value="master">master</option>
<option value="slave">slave</option>
</select>
</div>
<div id="result">
</div>
<button onclick="loadData()">加载数据</button>
<script> function loadData(){
$.get("/users",function (data){
$("#result").html(JSON.stringify(data)) }) } function dfChange(value){
$.post("/dstype",{
dsType:value}) } </script>
</body>
</html>
边栏推荐
- Enumeration gets values based on parameters
- Web security - payload
- Value of APS application in food industry
- [cloud native and 5g] micro services support 5g core network
- JS implementation force deduction 71 question simplified path
- Basic knowledge of lists
- Error analysis ~csdn rebound shell error
- Special topic of rotor position estimation of permanent magnet synchronous motor -- Summary of position estimation of fundamental wave model
- 报错分析~csdn反弹shell报错
- [Yann Lecun likes the red stone neural network made by minecraft]
猜你喜欢

知识图谱之实体对齐二

Value of APS application in food industry

案例 ①|主机安全建设:3个层级,11大能力的最佳实践

枚举根据参数获取值

What programming do children learn?

持续测试(CT)实战经验分享
![[diy] self designed Microsoft makecode arcade, official open source software and hardware](/img/a3/999c1d38491870c46f380c824ee8e7.png)
[diy] self designed Microsoft makecode arcade, official open source software and hardware
![[DIY]如何制作一款個性的收音機](/img/fc/a371322258131d1dc617ce18490baf.jpg)
[DIY]如何制作一款個性的收音機

Oceanbase Community Edition OBD mode deployment mode stand-alone installation
Tencent T4 architect, Android interview Foundation
随机推荐
01 基础入门-概念名词
Tencent cloud database public cloud market ranks top 2!
Special topic of rotor position estimation of permanent magnet synchronous motor -- fundamental wave model and rotor position angle
Oceanbase Community Edition OBD mode deployment mode stand-alone installation
Introduction of Xia Zhigang
[cloud lesson] EI lesson 47 Mrs offline data analysis - processing OBS data through Flink
[weekly pit] information encryption + [answer] positive integer factorization prime factor
持续测试(CT)实战经验分享
[Yann Lecun likes the red stone neural network made by minecraft]
使用.Net驱动Jetson Nano的OLED显示屏
Crawler (14) - scrape redis distributed crawler (1) | detailed explanation
Qinglong panel white screen one key repair
String length limit?
In line elements are transformed into block level elements, and display transformation and implicit transformation
5. 无线体内纳米网:十大“可行吗?”问题
【每周一坑】正整数分解质因数 +【解答】计算100以内质数之和
APS taps home appliance industry into new growth points
Guangzhou's first data security summit will open in Baiyun District
B-杰哥的树(状压树形dp)
Deep learning classification network -- zfnet