文章目录
  1. 1. 导读
  2. 2. AbstractRoutingDataSource
  3. 3. 实现
    1. 3.1. 线程隔离ThreadLocal
    2. 3.2. 动态数据源实现
    3. 3.3. 多数据源配置
    4. 3.4. AOP动态切换
  4. 4. 思考
  5. 5. 源码
  6. 6. 另外一种实现方式

导读

  • 平常的工作中可能大家的接触的都是单数据源的操作,很少能够用到多数据源的操作,但是在和其他系统对接的或者系统数据迁移时候往往需要定时从其他系统拉数据,此时多数据源变得很有必要了。
  • 多数据源的实现有两种方式,第一种是定义分别定义多个数据源,单个数据源分别和Mybatis整合,使用单独的事务机制;第二种是使用动态切换数据源的方式,笔者今天讲的就是动态数据源的切换。

AbstractRoutingDataSource

  • 见名知意,动态路由数据源,实现的原理其实很简单,内部使用一个Map结构,将DataSource存放其中,key是beanName,value是实例对象。在getConnection的时候动态判断当前的数据源。
  • 抽象类必须实现的方法是protected Object determineCurrentLookupKey(),返回值将作为key从Map中获取数据源。
  • 重要一点就是每个请求要保持线程隔离,不能一个请求切换了数据源影响了另外一个请求,因此我们需要将每一个线程的数据源保存在ThreadLocal中做到线程隔离。

实现

  • 针对业务逻辑的横向切面,切换数据源使用AOP的方式更加清晰一点。

线程隔离ThreadLocal

  • 定一个工具类,存放单线程数据源,只是个人这么做而已,也可以和数据源实现类整合在一起。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @Description ThreadLocal保存数据源,保持线程隔离
* @Author CJB
* @Date 2020/3/12 14:29
*/
public class DynamicThreadLocalHolder {
//本地线程保存数据源
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
/**
* 设置线程数据源
*/
public static void setDataSource(String dataSource){
threadLocal.set(dataSource);
}
/**
* 获取本地线程的数据源,这里只是数据源的key
* @return
*/
public static String getDataSource(){
return threadLocal.get();
}
/**
* 清除数据源
*/
public static void clearDataSource(){
threadLocal.remove();
}
}

动态数据源实现

  • 实现AbstractRoutingDataSource,重写determineCurrentLookupKey方法即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Description 动态数据源的实现
* @Author CJB
* @Date 2020/3/12 14:27
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 决定使用哪个数据源
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
//从ThreadLocal中获取数据源的beanName
return DynamicThreadLocalHolder.getDataSource();
}
}

多数据源配置

  • 定义了两个数据源,笔者和Mybatis整合了,同时也整合了事务管理器(此处有待优化)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* @Description 数据源的配置
* @Author CJB
* @Date 2020/3/9 13:45
*/
@Configuration
@MapperScan(basePackages = {"com.vivachek.service.dao","com.vivachek.service.dao2"})
public class DatasourceConfig {
/**
* 注入数据源1
*/
@ConfigurationProperties(prefix = "spring.datasource1")
@Bean(value = "dataSource1")
public DataSource dataSource1() {
return new DruidDataSource();
}
/**
* 第二个数据源
*/
@Bean(name = "dataSource2")
@ConfigurationProperties(prefix = "spring.datasource2")
public DataSource dataSource2() {
return new DruidDataSource();
}
/**
* 动态数据源
*
* @return
*/
@Bean
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dataSource = new DynamicDataSource();
//默认数据源,在没有切换数据源的时候使用该数据源
dataSource.setDefaultTargetDataSource(dataSource2());
HashMap<Object, Object> map = Maps.newHashMap();
map.put("dataSource1", dataSource1());
map.put("dataSource2", dataSource2());
//设置数据源Map,动态切换就是根据key从map中获取
dataSource.setTargetDataSources(map);
return dataSource;
}
/**
* 和Mybatis整合必须设置SqlSessionFactory的数据源为动态数据源
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
//此处必须设置动态数据源
factoryBean.setDataSource(dynamicDataSource());
factoryBean.setVfs(SpringBootVFS.class);
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
factoryBean.setConfiguration(configuration);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**/*.xml"));
return factoryBean.getObject();
}
/**
* 设置事务管理器
*
* @return
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}

AOP动态切换

  • 使用注解+AOP的方式动态切换数据源,在使用注解的方法上切换指定的数据源
  • 注解如下:
1
2
3
4
5
6
7
8
9
10
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ChangeSource {
/**
* 数据源,动态数据源默认的是datasource1,这里默认的直接dataSource2
* @return
*/
String value() default "dataSource1";
}
  • 切面如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @Description 切面
* @Author CJB
* @Date 2020/3/12 16:18
*/
@Aspect
@Component
@Slf4j
public class ChangeSourceAspect {
@Pointcut("@annotation(com.vivachek.core.aop.ChangeSource)")
public void pointcut() {
}
/**
* 在方法执行之前往ThreadLocal中设置值
*/
@Before(value = "pointcut()")
public void beforeOpt(JoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
ChangeSource changeSource = method.getAnnotation(ChangeSource.class);
log.info("[Switch DataSource]:" + changeSource.value());
DynamicThreadLocalHolder.setDataSource(changeSource.value());
}
/**
* 结束之后清除
*/
@After(value = "pointcut()")
public void afterOpt() {
DynamicThreadLocalHolder.clearDataSource();
log.info("[change Default DataSource]");
}
}
  • 至此,整合完成,在需要切换数据源的方法上使用@ChangeSource注解即可。

思考

  • 此处有几个问题供读者思考,如下:
    • 在多数据源配置的地方其实可以更加简单,如何优化?
    • 在声明DynamicDataSource的方式如果加上了@Primary注解为什么会出现循环依赖的异常?【巨坑】
    • AOP切面为什么在方法嵌套调用会失效,如何解决?
    • 动态数据源的事务如何实现?
  • 以上问题小伙伴可以思考一下,后续会更新出来…….

源码

  • 源码已经上传,搜索微信公众号【码猿技术专栏】或者扫描下面的二维码关注后,关键词回复

【数据源】即可。

另外一种实现方式

  • 关于另外一种实现方式,之前讲过SpringBoot整合JTA这篇文章中就是用两个数据源分别和Mybatis整合,关注公众号查看历史文章【SpringBoot整合JTA】
文章目录
  1. 1. 导读
  2. 2. AbstractRoutingDataSource
  3. 3. 实现
    1. 3.1. 线程隔离ThreadLocal
    2. 3.2. 动态数据源实现
    3. 3.3. 多数据源配置
    4. 3.4. AOP动态切换
  4. 4. 思考
  5. 5. 源码
  6. 6. 另外一种实现方式