多數據源動態切換及數據源切換單事務混亂解決


一、AbstractRoutingDataSource
Spring boot提供了AbstractRoutingDataSource 根據用戶定義的規則選擇當前的數據源,這樣我們可以在執行查詢之前,設置使用的數據源。實現可動態路由的數據源,在每次數據庫查詢操作前執行。它的抽象方法 determineCurrentLookupKey() 決定使用哪個數據源。

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 源碼的介紹:

大概意思是:
AbstractRoutingDataSource的getConnection() 方法根據查找 lookup key 鍵對不同目標數據源的調用,通常是通過(但不一定)某些線程綁定的事物上下文來實現。

AbstractRoutingDataSource的多數據源動態切換的核心邏輯是:在程序運行時,把數據源數據源通過 AbstractRoutingDataSource 動態織入到程序中,靈活的進行數據源切換。
基於AbstractRoutingDataSource的多數據源動態切換,可以實現讀寫分離,這么做缺點也很明顯,無法動態的增加數據源。

實現邏輯:

定義DynamicDataSource類繼承抽象類AbstractRoutingDataSource,並實現了determineCurrentLookupKey()方法。
把配置的多個數據源會放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通過afterPropertiesSet()方法將數據源分別進行復制到resolvedDataSources和resolvedDefaultDataSource中。
調用AbstractRoutingDataSource的getConnection()的方法的時候,先調用determineTargetDataSource()方法返回DataSource在進行getConnection()。
二、具體實現
1、pom.xml
新建springboot項目,其中pom.xml 文件依賴如下

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.github.caspar-chen</groupId>
<artifactId>swagger-ui-layer</artifactId>
<version>1.1.3</version>
</dependency>
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
如果不要swagger可以去掉swagger依賴

2、使用mybatis plus 生成實體與xml等代碼
3、在spring boot 啟動類上添加掃描mapper注解 - @MapperScan(“com.xh.mapper”)
4、在配置文件 application.properties 中添加多個(我這里是兩個)數據源的配置信息
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
# 數據源1
spring.datasource.druid.first.url=jdbc:mysql://localhost:3306/test1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.druid.first.username=root
spring.datasource.druid.first.password=****
# 數據源2 需要創建對應數據庫 更改該庫中 sys_user 表
spring.datasource.druid.second.url=jdbc:mysql://localhost:3306/test2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.druid.second.username=root
spring.datasource.druid.second.password=****
1
2
3
4
5
6
7
8
9
10
如果還要添加數據源就按照 這種格式繼續往下寫。

5、集成動態數據源模塊
5.1、新建注解 CurDataSource 指定要使用的數據源
/**
* 多數據源注解
* <p/>
* 指定要使用的數據源
*
* @author xiaohe
* @version V1.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurDataSource {

String name() default "";

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
5.2、新建常量存儲於獲取數據源
/**
* 增加多數據源,在此配置
*
* @author xiaohe
* @version V1.0.0
*/
public interface DataSourceNames {

String FIRST = "first";

String SECOND = "second";

}
1
2
3
4
5
6
7
8
9
10
11
12
13
5.3、新建類 DynamicDataSource
DynamicDataSource擴展Spring的AbstractRoutingDataSource抽象類,重寫 determineCurrentLookupKey() 方法


import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.Map;

/**
* 擴展 Spring 的 AbstractRoutingDataSource 抽象類,重寫 determineCurrentLookupKey 方法
* 動態數據源
* determineCurrentLookupKey() 方法決定使用哪個數據源
*
* @author xiaohe
* @version V1.0.0
*/
public class DynamicDataSource extends AbstractRoutingDataSource {

/**
* ThreadLocal 用於提供線程局部變量,在多線程環境可以保證各個線程里的變量獨立於其它線程里的變量。
* 也就是說 ThreadLocal 可以為每個線程創建一個【單獨的變量副本】,相當於線程的 private static 類型變量。
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

/**
* 決定使用哪個數據源之前需要把多個數據源的信息以及默認數據源信息配置好
*
* @param defaultTargetDataSource 默認數據源
* @param targetDataSources 目標數據源
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}

@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}

public static void setDataSource(String dataSource) {
CONTEXT_HOLDER.set(dataSource);
}

public static String getDataSource() {
return CONTEXT_HOLDER.get();
}

public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}

}

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
5.4、新建多數據源配置類
配置多數據源的信息,生成多個(我這里是兩個,對應application.properties中定義的數據源)數據源

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
* 配置多數據源
* @author xiaohe
* @version V1.0.0
*/
@Configuration
public class DynamicDataSourceConfig {

@Bean
@ConfigurationProperties("spring.datasource.druid.first")
public DataSource firstDataSource(){

return DruidDataSourceBuilder.create().build();
}

@Bean
@ConfigurationProperties("spring.datasource.druid.second")
public DataSource secondDataSource(){

return DruidDataSourceBuilder.create().build();
}

@Bean
@Primary
public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>(5);
targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
return new DynamicDataSource(firstDataSource, targetDataSources);
}

}

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
5.5、采用aop的方式,在需要修改數據源的地方使用注解方式去切換,然后切面修改ThreadLocal的內容

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
* 多數據源,切面處理類
*
* @author xiaohe
* @version V1.0.0
*/
@Slf4j
@Aspect
@Component
public class DataSourceAspect implements Ordered {

@Pointcut("@annotation(com.xh.datasource.CurDataSource)")
public void dataSourcePointCut() {

}

@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();

CurDataSource ds = method.getAnnotation(CurDataSource.class);
if (ds == null) {
DynamicDataSource.setDataSource(DataSourceNames.FIRST);
log.debug("set datasource is " + DataSourceNames.FIRST);
} else {
DynamicDataSource.setDataSource(ds.name());
log.debug("set datasource is " + ds.name());
}

try {
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
log.debug("clean datasource");
}
}

@Override
public int getOrder() {
return 1;
}
}

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
5.6、啟動類上添加數據源配置
因為數據源是自己生成的,所以要去掉原先springboot啟動時候自動裝配的數據源配置。

import com.xh.datasource.DynamicDataSourceConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Import;

/**
* @author xiaohe
*/
@MapperScan("com.xh.mapper")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@Import({DynamicDataSourceConfig.class})
public class SpringBootDynamicDataSourceApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootDynamicDataSourceApplication.class, args);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
5.7、測試數據源切換
service中定義兩個查詢,分別查兩個數據庫:

import com.xh.entity.SysUser;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* <p>
* 系統用戶 服務類
* </p>
*
* @author xiaohe
* @since 2019-06-04
*/
public interface SysUserService extends IService<SysUser> {

SysUser findUserByFirstDb(long id);

SysUser findUserBySecondDb(long id);

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
實現類:因為默認是使用第一個數據源,所以不用注解,使用數據源二需要添加注解 @CurDataSource(name = DataSourceNames.SECOND) 。


import com.xh.datasource.CurDataSource;
import com.xh.datasource.DataSourceNames;
import com.xh.entity.SysUser;
import com.xh.mapper.SysUserMapper;
import com.xh.service.SysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
* <p>
* 系統用戶 服務實現類
* </p>
*
* @author xiaohe
* @since 2019-06-04
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

@Override
public SysUser findUserByFirstDb(long id) {
return this.baseMapper.selectById(id);
}

@CurDataSource(name = DataSourceNames.SECOND)
@Override
public SysUser findUserBySecondDb(long id) {
return this.baseMapper.selectById(id);
}

}

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
測試類


import com.xh.entity.SysUser;
import com.xh.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootDynamicCurDataSourceApplicationTests {

@Autowired
private SysUserService userService;

@Test
public void test() {
SysUser user = userService.findUserByFirstDb(1);
log.info("第一個數據庫 : [{}]", user.toString());
SysUser user2 = userService.findUserBySecondDb(1);
log.info("第二個數據庫 : [{}]", user2.toString());
}
}

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
建表與初始化sql:

-- 系統用戶
CREATE TABLE `sys_user`
(
`user_id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用戶名',
`password` varchar(100) COMMENT '密碼',
`salt` varchar(20) COMMENT '鹽',
`email` varchar(100) COMMENT '郵箱',
`mobile` varchar(100) COMMENT '手機號',
`status` tinyint COMMENT '狀態 0:禁用 1:正常',
`create_user_id` bigint(20) COMMENT '創建者ID',
`create_time` datetime COMMENT '創建時間',
PRIMARY KEY (`user_id`),
UNIQUE INDEX (`username`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 COMMENT ='系統用戶';

-- 初始數據
INSERT INTO `sys_user` (`user_id`, `username`, `password`, `salt`, `email`, `mobile`, `status`, `create_user_id`,
`create_time`)
VALUES ('1', 'admin', '9ec9750e709431dad22365cabc5c625482e574c74adaebba7dd02f1129e4ce1d', 'YzcmCZNvbXocrsz9dm8e',
'root@renren.io', '13612345678', '1', '1', '2016-11-11 11:11:11');

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
把庫2的username改為:admin2222

最后的測試結果打印如下:


三、關於事務
AbstractRoutingDataSource 只支持單庫事務,也就是說切換數據源要在開啟事務之前執行。 spring DataSourceTransactionManager進行事務管理,開啟事務,會將數據源緩存到DataSourceTransactionObject對象中進行后續的commit rollback等事務操作。

出現多數據源動態切換失敗的原因是因為在事務開啟后,數據源就不能再進行隨意切換了,也就是說,一個事務對應一個數據源。

傳統的Spring管理事務是放在Service業務層操作的,所以更換數據源的操作要放在這個操作之前進行。也就是切換數據源操作放在Controller層,可是這樣操作會造成Controller層代碼混亂的結果。

故而想到的解決方案是將事務管理在數據持久 (Dao層) 開啟,切換數據源的操作放在業務層進行操作,就可在事務開啟之前順利進行數據源切換,不會再出現切換失敗了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM