引言
哈嘍,小夥伴們,一週不見了,在這段時間,我利用下班的閒暇時間更新了一版代碼生成器,添加了之前呼聲較高的多數據源模式,這樣生成的代碼可以實現動態切換數據源的功能,多數據源在項目當中還算比較常用的,例如主從讀寫分離,多庫操作等都需要在同一個項目中操作多個數據庫,本次更新正是解決了這個痛點,生成代碼之後,可以通過註解的方式靈活切換數據源,並且支持多庫事務一致性,下面就讓我們一起看一下具體的實現效果,順便講一下動態多數據源的內部原理!
生成器界面調整
爲了實現多數據源模式,代碼生成器對界面進行了調整,如下:
主界面添加了選擇數據源的功能,並且現在數據庫信息需要點擊數據源配置來進行配置,點擊後會彈出如下窗口:
在這裏我們可以配置數據庫信息,配置完畢後點擊保存,在主界面即可進行選擇,在主界面被選擇的數據源將在生成的代碼中作爲默認數據源使用。
勾選多數據源模式可以生成多數據源模式代碼,不勾選則與之前一樣,生成的是常規單數據源項目。
總體跟原來區別不大,使用多數據源模式生成代碼基本步驟如下:
- 配置數據源保存
- 主界面依次選擇數據源,配置數據項信息
- 勾選多數據源模式,點擊生成代碼即可
生成代碼展示
多數據源模式下會在 config 包下生成多數據源相關的配置類及切面,如果大家有個性化需求可以通過修改 DynamicDataSourceAspect 切面來實現動態切換邏輯,現有切換邏輯基本足夠。
多數據源其實還可以通過代碼分包的方式實現,這種方式實現起來易於理解:配置多個數據源,掃描不同的包,創建屬於自己的 sqlSessionFactory 和 txManager(事務管理器),在使用的時候可以通過調用不同包下的 mapper 來實現多數據源的效果,但是這種方式的弊端也較爲明顯,分包稍有不慎便會出錯,並且如果想要實現不同數據源下的事務一致性也較爲麻煩,在同一個 service 方法中操作多個數據庫因此受限。
動態多數據源則不會有以上問題,因此代碼生成器選擇了動態多數據源的生成模式,利用 aop 實現數據源的動態切換,並且可以保證多庫操作事務一致性,後面會詳細講解。
代碼運行效果
在 idea 中運行生成的代碼,啓動完畢登錄,點擊左側菜單查詢:
查看後臺日誌,發現會切換不同的數據庫執行sql:
下面以 springboot 爲例,講一下多數據源內部原理。
動態多數據源內部原理及核心代碼
動態多數據源的內部原理其實就是 aop,只不過複雜的是 aop 的實現過程。
mybatis 爲我們提供了一個抽象類 AbstractRoutingDataSource,通過繼承此類,重寫 determineCurrentLookupKey 方法可以根據返回值決定當前使用哪個數據源,因此我們創建類 DynamicDataSource 繼承 AbstractRoutingDataSource 並重寫 determineCurrentLookupKey 方法:
/**
* 重寫數據源選擇方法(獲取當前線程設置的數據源)
* @author zrx
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
}
}
先不忙着實現,如果想要正確匹配數據源,我們還需要向 DynamicDataSource 類中註冊數據源,所以需要先對數據源進行配置,這裏註冊兩個數據源 db1(mysql) 和 db2(oracle),我們使用枚舉值 DB1 和 DB2 作爲數據源 db1 和 db2 的 key:
package mutitest.config.mutidatasource;
/**
* 數據源枚舉
* @author zrx
*/
public enum DataSourceType {
/**
* DB1
*/
DB1,
/**
* DB2
*/
DB2,
}
package mutitest.config.mutidatasource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 數據源配置類
*
* @author zrx
*/
@Configuration
public class DynamicDataSourceConfig {
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.db1")
public DataSource db1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.db2")
public DataSource db2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource dynamicDataSource(@Qualifier(value = "db1") DataSource db1,@Qualifier(value = "db2") DataSource db2) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
//設置默認數據源
dynamicDataSource.setDefaultTargetDataSource(db1);
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceType.DB1, db1);
dataSourceMap.put(DataSourceType.DB2, db2);
//向動態數據源中註冊所有數據源信息
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
//返回動態數據源的事務管理器
return new DataSourceTransactionManager(dataSource);
}
}
通過以上配置,我們成功向 DynamicDataSource 中註冊了 db1 和 db2,如何才能獲取當前程序運行中的數據源呢?這就需要我們用到 ThreadLocal,ThreadLocal 可以向當前線程中 set 和 get 值並且不受其他線程影響,而我們服務器的每一個請求都由一個工作線程來處理(nio 模式也是一個請求一個工作線程處理,只是在接收請求的時候使用了 io 多路複用),所以可以使用 ThreadLocal 存儲當前工作線程的數據源,ThreadLocal 在很多開源框架中都有使用,主要用於線程隔離。
創建 DynamicDataSourceHolder 類,存儲當前線程中的數據源:
package mutitest.config.mutidatasource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 數據源選擇器
*
* @author zrx
*/
public class DynamicDataSourceHolder {
private static final ThreadLocal<DataSourceType> DATA_SOURCE_HOLDER = new ThreadLocal<>();
private static final Set<DataSourceType> DATA_SOURCE_TYPES = new HashSet<>();
static {
//添加全部枚舉
DATA_SOURCE_TYPES.addAll(Arrays.asList(DataSourceType.values()));
}
public static void setType(DataSourceType dataSourceType) {
if (dataSourceType == null) {
throw new NullPointerException();
}
DATA_SOURCE_HOLDER.set(dataSourceType);
}
public static DataSourceType getType() {
return DATA_SOURCE_HOLDER.get();
}
static void clearType() {
DATA_SOURCE_HOLDER.remove();
}
static boolean containsType(DataSourceType dataSourceType) {
return DATA_SOURCE_TYPES.contains(dataSourceType);
}
}
然後,實現 determineCurrentLookupKey 方法,一行代碼即可:
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getType();
}
最後一步,我們要實現數據源的動態切換,則需要自己實現一個數據源動態切面,改變當前線程中的數據源,我們可以使用註解來輔助實現,在切面中通過掃描方法上的註解來得知具體切換到哪個數據源。
創建 DBType 註解:
package mutitest.config.mutidatasource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 多數據源註解
* @author zrx
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DBType {
DataSourceType value() default DataSourceType.DB1;
}
創建數據源動態切面 DynamicDataSourceAspect:
package mutitest.config.mutidatasource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 動態數據源切面(order 必須要設置,否則事務的切面會優先執行,數據源已經設置完了,再設置就無效了)
* @author zrx
*/
@Aspect
@Component
@Order(1)
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
@Before("@annotation(dbType)")
public void changeDataSourceType(JoinPoint joinPoint, DBType dbType) {
DataSourceType curType = dbType.value();
//判斷註解類型
if (!DynamicDataSourceHolder.containsType(curType)) {
logger.info("指定數據源[{}]不存在,使用默認數據源-> {}", dbType.value(), joinPoint.getSignature());
} else {
logger.info("use datasource {} -> {}", dbType.value(), joinPoint.getSignature());
// 切換當前線程的數據源
DynamicDataSourceHolder.setType(dbType.value());
}
}
@After("@annotation(dbType)")
public void restoreDataSource(JoinPoint joinPoint, DBType dbType) {
logger.info("use datasource {} -> {}", dbType.value(), joinPoint.getSignature());
//方法執行完,清空,防止內存泄漏
DynamicDataSourceHolder.clearType();
}
}
在數據源切面上需要添加 @Order 註解,值取1,這是因爲之前我們配置了動態數據源事務,spring 會因此生成事務代理並且會優先於切面執行,事務代理一旦生成,數據源便被固定,這樣我們在切面中切換數據源就會無效,所以切面邏輯需要在事務代理之前執行纔可生效。
至此,動態多數據源基本實現完畢!
事務一致性問題
使用動態多數據源的同時,也要注意保證事務一致性,大家可能遇到這種情況,傳統單數據源應用中,同一個 service ,在沒有開啓事務的方法裏調用開啓事務的方法會導致事務失效,這是因爲 spring 只會對相同的 service 代理一次,否則如果在沒有開啓事務的方法中再次開啓自身代理會導致循環依賴問題出現,類似 “無限套娃”:自己代理的方法調用自己代理的另一個方法,並且另一個方法還需要自己的代理。解決此類問題的方法很簡單,讓調用方開啓事務即可,多數據源模式中同樣適用。
除此之外,多數據源模式中還存在如下場景:serviceA 中的 A 和 B 方法都開啓了事務,但操作的是不同的數據庫(ip不同),這個時候 A 調用 B,使用的是 A 的代理,對 B 不適用,便會報錯,對此我們可以把 B 方法移入另一個 serviceB 中,在 serviceA 中注入 serviceB ,在 A 方法中使用 serviceB 調用 B 方法,這樣執行到 B 方法的時候使用的便是 serviceB 的代理,看起來沒有問題,但還有一點遺漏,那就是事務的傳播行爲。
我們都知道,Spring 中默認的事務傳播行爲是 required:如果需要開啓事務,則開啓事務,如果已經開啓事務,則加入當前事務。上文中,執行 B 方法的時候雖然使用的是 serviceB 的代理,但是由於其事務傳播行爲是 required,A 方法執行的時候已經開啓了事務,所以導致 B 方法加入到了 A 方法的事務中,但 A 和 B 屬於兩個不同的數據庫,使用相同的事務管理器必然會出現問題。爲了解決此問題,我們可以把事務傳播行爲改爲 required_new:如果需要開啓事務,則開啓事務,並且總是開啓新的事務。這樣執行 B 方法的時候會開啓新的事務,使用的便是 B 所在數據庫的事務管理器,B 方法也就可以正常執行了,並且如果 B 出現異常,如果 A 不主動捕獲,則 A,B 都會回滾。
也許有人會問,單數據源模式下使用 required 爲什麼不會有上述問題呢,因爲單數據源模式下使用的是同一個數據庫,在事務執行過程中,當前事務是共享且通用的,所以沒問題。除此之外,使用 required 不必頻繁重開事務,也一定程度上提升了系統性能,多數據源模式下由於不同數據庫之間事務是完全隔離的,所以才需要使用 required_new 重開事務,當然,也需要根據業務具體場景具體分析,這裏討論的只是較爲通用的情況。
代碼生成器多數據源模式下使用的事務傳播行爲正是 required_new,全局配置類如下:
package mutitest.config;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;
/**
* 全局事務支持
*
* @author zrx
*
*/
@Aspect
@Configuration
public class TransactionAdviceConfig {
private static final String AOP_POINTCUT_EXPRESSION = "execution(* mutitest.service.impl.*.*(..))";
@Autowired
private PlatformTransactionManager transactionManager;
@Bean
public TransactionInterceptor txAdvice() {
DefaultTransactionAttribute txAttr_REQUIRED = new DefaultTransactionAttribute();
txAttr_REQUIRED.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
DefaultTransactionAttribute txAttr_REQUIRED_READONLY = new DefaultTransactionAttribute();
txAttr_REQUIRED_READONLY.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
txAttr_REQUIRED_READONLY.setReadOnly(true);
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
//可以根據業務需要自行添加需要被事務代理的方法
source.addTransactionalMethod("add*", txAttr_REQUIRED);
source.addTransactionalMethod("delete*", txAttr_REQUIRED);
source.addTransactionalMethod("update*", txAttr_REQUIRED);
source.addTransactionalMethod("select*", txAttr_REQUIRED_READONLY);
source.addTransactionalMethod("likeSelect*", txAttr_REQUIRED_READONLY);
return new TransactionInterceptor(transactionManager, source);
}
@Bean
public Advisor txAdviceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice());
}
}
到此爲止,我們纔算實現了一個完整的動態多數據源功能,可見是有許多技術細節潛藏在裏面的,朋友們可以使用代碼生成器生成多數據源模式下的代碼自行運行體會。
結語
本文到這裏就結束了,寫這個多數據源生成功能其實也算花了一番心思,正着寫代碼容易,反過來生成是真不容易,並且由於最開始做的時候沒有考慮到多數據源的情況,導致最開始的設計全都是針對單個數據庫的,這次強行在外面包了一層,總歸是實現了,在這個過程中,順便也複習了一下 Spring 的循環依賴,Bean 加載週期等老生常談的問題,也算有所收穫。作爲開發人員,我們要多關注一些功能底層的東西,而不是簡單的 api 調用,這樣才能不斷突破瓶頸,取得成長。碼字不易,各位看官可以點贊,在看,星標關注哦,我們下次再見!
關注公衆號 螺旋編程極客
獲取代碼生成器最新動態,同時第一時間解鎖更多精彩內容!