在項目中,有這麼一些場景,需要去考慮數據源路由的事情,比如說
-
因爲系統絕對併發量太高,單個數據庫實例難以應對,需要分庫
-
因爲系統數據量太大,單表難以應對,需要分表
-
大多數系統呈現出讀多寫少業務特點,爲了緩解存儲層壓力,通過搭建主從集羣(一寫多讀),實現讀寫分離,提升系統響應能力,進一步提升用戶體驗
-
報表類應用,可能會需要聚合多個數據源,從而獲取數據
你看到了,以上不管是分庫、分表、還是主從實現讀寫分離,都表現出了一個應用,對應多個數據庫實例的場景。
那麼這個時候,我們就需要去考慮多數據源路由的事情了。在具體實現方案上,有兩種比較常見的選擇方案
-
自己在應用層面,去實現數據源路由,比如說spring框架提供了AbstractRoutingDataSource,我們只要擴展它就可以了
-
或者在應用層,與數據源之間,增加代理proxy的實現方案,比如說業界使用較多的MyCat、ShardingSphere等
這兩種方案,具體該如何去選擇呢?如果我們是中小型的團隊,應用規模也不是很大,那麼適合選擇在應用層面實現數據源路由的方案,主要理由
-
運維、DBA小夥伴成員不多,且對proxy方案中的代理中間件沒有深入的研究,沒有辦法做到一旦碰到問題,可以迅速解決(甚至可能解決不了)
-
在應用層面自己實現的數據源路由,代碼都是自己的寫的,一旦有什麼問題,研發小夥伴直接就解決了
基於以上,反過來我們就可以考慮採用proxy的代理方案了。方便你理解,還是上一個圖吧,典型的主從集羣,讀寫分離架構
1.準備環境
通過以上描述,我們知道在實現多數據路由上,有兩種可選的方案,本篇文章我們分享方案一的實現,即擴展spring提供的AbstractRoutingDataSource。
首先來準備環境,我在本地準備了兩個數據庫實例
這兩個庫雖然本身沒有直接的關係,我們假設這是一個報表類應用(聚合多個數據源),且我們的重點是實現在數據源之間的路由,因此不影響。
在本案例的實現中
-
將user-center庫作爲讀庫,即當有讀操作的請求進來,將請求路由到user-center庫
-
將training庫作爲寫庫,即當有更新請求進來,將請求路由到training庫
導入依賴
<dependencies>
<!--web 依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--aop 依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--jdbc 依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mybatis 依賴-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--druid 依賴-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<!-- mysql驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok依賴-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2.核心配置
本案例持久層,選擇的是mybatis框架,相關核心配置如下
2.1.application.yml
server:
port: 8080
spring:
application:
name: follow-me-springboot-multidatasource
#數據源配置
mysql:
datasource:
type-aliases-package: cn.edu.anan.entity
mapper-locations: classpath:mybatis/mapper/*Mapper.xml
config-location: classpath:mybatis/sqlMapConfig.xml
write:
url: jdbc:mysql://127.0.0.1:3320/training?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: admin
driver-class-name: com.mysql.cj.jdbc.Driver
read:
url: jdbc:mysql://127.0.0.1:3310/user-center?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: admin
driver-class-name: com.mysql.cj.jdbc.Driver
2.2.sqlMapConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 是否開啓緩存 -->
<setting name="cacheEnabled" value="true" />
<!-- 是否開啓延遲加載 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 是否開啓延遲加載 -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 是否允許單條sql 返回多個數據集 -->
<setting name="multipleResultSetsEnabled" value="true" />
<!-- 是否開啓列別名 -->
<setting name="useColumnLabel" value="true" />
<!-- 是否使用JDBC自增主鍵 -->
<setting name="useGeneratedKeys" value="false" />
<!-- MyBatis 自動映射策略,NONE:不隱射 PARTIAL:部分 FULL:全部 -->
<setting name="autoMappingBehavior" value="PARTIAL" />
<!-- 設置默認執行器Executor -->
<setting name="defaultExecutorType" value="SIMPLE" />
<!--語句超時時間-->
<setting name="defaultStatementTimeout" value="25" />
<!--默認抓取大小-->
<setting name="defaultFetchSize" value="100" />
<!--分頁相關-->
<setting name="safeRowBoundsEnabled" value="false" />
<!-- 使用駝峯命名轉換 -->
<setting name="mapUnderscoreToCamelCase" value="true" />
<!-- 設置本地緩存範圍 session:就會有數據的共享 -->
<setting name="localCacheScope" value="SESSION" />
<!-- 默認爲OTHER,爲了解決oracle插入null報錯的問題要設置爲NULL -->
<setting name="jdbcTypeForNull" value="NULL" />
<!--延遲加載觸發方法列表-->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
</settings>
</configuration>
2.3.項目代碼結構
3.核心代碼
擴展AbstractRoutingDataSource實現多數據源之間路由,核心是
-
配置各個數據源
-
通過ThreadLocal實現線程數據源上下文綁定
-
通過AOP進行線程上下文數據源路由信息設置
3.1.數據源配置DataSourceConfig
/**
* 數據源配置
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 15:55
*/
@Configuration
@MapperScan(basePackages = "cn.edu.anan.dao", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
/**
* 包掃描別名
*/
@Value("${mysql.datasource.type-aliases-package}")
private String typeAliasesPackage;
/**
*mapper映射文件位置
*/
@Value("${mysql.datasource.mapper-locations}")
private String mapperLocation;
/**
*mybatis配置文件位置
*/
@Value("${mysql.datasource.config-location}")
private String configLocation;
/**
* 寫數據源
* @return
*/
@Primary
@Bean
@ConfigurationProperties(prefix = "mysql.datasource.write")
public DataSource writeDataSource() {
return new DruidDataSource();
}
/**
* 讀數據源
* @return
*/
@Bean
@ConfigurationProperties(prefix = "mysql.datasource.read")
public DataSource readDataSource() {
return new DruidDataSource();
}
/**
* 配置sqlSessionFactory
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(routingDataSource());
bean.setTypeAliasesPackage(typeAliasesPackage);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources(mapperLocation));
bean.setConfigLocation(resolver.getResource(configLocation));
return bean.getObject();
}
/**
* 設置數據源路由表
* @return
*/
@Bean
public AbstractRoutingDataSource routingDataSource() {
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>(2);
targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
targetDataSources.put(DbContextHolder.READ, readDataSource());
proxy.setDefaultTargetDataSource(writeDataSource());
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
/**
* 配置事務管理器
* @return
*/
@Bean
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(routingDataSource());
}
}
3.2.數據源線程上下文DbContextHolder
/**
* 數據源上下文環境
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 16:02
*/
@Slf4j
public class DbContextHolder {
/**
* 寫數據源標識
*/
public static final String WRITE = "write";
/**
* 讀數據源標識
*/
public static final String READ = "read";
/**
* 本地線程綁定
*/
private static ThreadLocal<String> contextHolder= new ThreadLocal<>();
/**
* 設置數據源類型
* @param dbType
*/
public static void setDbType(String dbType) {
if (dbType == null) {
log.error("dbType爲空");
throw new NullPointerException();
}
log.info("設置dbType爲:{}",dbType);
contextHolder.set(dbType);
}
/**
* 獲取數據源類型
* @return
*/
public static String getDbType() {
return contextHolder.get() == null ? WRITE : contextHolder.get();
}
/**
* 清除ThreadLocal
*/
public static void clearDbType() {
contextHolder.remove();
}
}
3.3.擴展數據源路由MyAbstractRoutingDataSource
/**
* 數據源路由,擴展AbstractRoutingDataSource
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 16:05
*/
@Slf4j
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource{
/**
* 返回數據源路由key
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
String dbKey = DbContextHolder.getDbType();
if (dbKey == DbContextHolder.WRITE) {
log.info("當前更新動作,走主庫");
return dbKey;
}
log.info("當前讀取操作,走從庫");
return DbContextHolder.READ;
}
}
3.4.註解DataSourceSwitcher
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSwitcher {
/**
* 默認數據源
* @return
*/
String value() default "write";
/**
* 清除
* @return
*/
boolean clear() default true;
}
3.5.切面ReadOnlyAspect
/**
* 讀數據源切面
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 17:07
*/
@Aspect
@Component
@Slf4j
public class ReadOnlyAspect implements Ordered{
/**
* 線程上下文設置讀數據源
* @param pjp
* @param read
* @return
* @throws Throwable
*/
@Around("@annotation(read)")
public Object setRead(ProceedingJoinPoint pjp, DataSourceSwitcher read) throws Throwable{
try{
DbContextHolder.setDbType(DbContextHolder.READ);
return pjp.proceed();
}finally {
DbContextHolder.clearDbType();
log.info("清除threadLocal");
}
}
/**
* 順序
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
4.使用案例
在數據源路由配置中,設置了默認的數據源是:寫數據源
如果是寫入操作,默認應用代碼不需要特殊處理;如果讀操作,應用代碼方法上,需要加上@DataSourceSwitcher(value="read")註解,比如
那麼在運行時,通過切面綁定線程上下文數據源信息
準備兩個測試案例
-
寫入訂單
-
讀取全部用戶列表
/**
* controller
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 15:46
*/
@RestController
@RequestMapping("route")
@Slf4j
public class MultiDataSourceController {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
/**
* 寫數據源測試:寫入一個訂單
* @param order
* @return
*/
@RequestMapping("write")
public Order write(@RequestBody Order order){
orderService.insertOne(order);
return order;
}
/**
* 讀數據源測試:查詢全部用戶列表數據
* @return
*/
@RequestMapping("read")
public List<User> read(@RequestBody User user){
log.info("查詢條件:{}", user);
return userService.selectAll(user);
}
啓動應用,分別訪問端點
觀察控制檯輸出
案例輸出讀操作,走從庫;寫操作,走主庫。我們看到已經實現多數據源路由,最後本文源碼,請參考:https://gitee.com/yanghouhua/springboot.git,子模塊: follow-me-springboot-multidatasource