引言
哈嘍,小伙伴們市栗,一周不見了缀拭,在這段時間咳短,我利用下班的閑暇時間更新了一版代碼生成器,添加了之前呼聲較高的多數(shù)據(jù)源模式蛛淋,這樣生成的代碼可以實現(xiàn)動態(tài)切換數(shù)據(jù)源的功能咙好,多數(shù)據(jù)源在項目當(dāng)中還算比較常用的,例如主從讀寫分離褐荷,多庫操作等都需要在同一個項目中操作多個數(shù)據(jù)庫勾效,本次更新正是解決了這個痛點,生成代碼之后叛甫,可以通過注解的方式靈活切換數(shù)據(jù)源层宫,并且支持多庫事務(wù)一致性,下面就讓我們一起看一下具體的實現(xiàn)效果其监,順便講一下動態(tài)多數(shù)據(jù)源的內(nèi)部原理萌腿!
生成器界面調(diào)整
為了實現(xiàn)多數(shù)據(jù)源模式,代碼生成器對界面進(jìn)行了調(diào)整抖苦,如下:
主界面添加了選擇數(shù)據(jù)源的功能毁菱,并且現(xiàn)在數(shù)據(jù)庫信息需要點擊數(shù)據(jù)源配置來進(jìn)行配置,點擊后會彈出如下窗口:
在這里我們可以配置數(shù)據(jù)庫信息锌历,配置完畢后點擊保存贮庞,在主界面即可進(jìn)行選擇,在主界面被選擇的數(shù)據(jù)源將在生成的代碼中作為默認(rèn)數(shù)據(jù)源使用辩涝。
勾選多數(shù)據(jù)源模式可以生成多數(shù)據(jù)源模式代碼贸伐,不勾選則與之前一樣,生成的是常規(guī)單數(shù)據(jù)源項目怔揩。
總體跟原來區(qū)別不大捉邢,使用多數(shù)據(jù)源模式生成代碼基本步驟如下:
- 配置數(shù)據(jù)源保存
- 主界面依次選擇數(shù)據(jù)源,配置數(shù)據(jù)項信息
- 勾選多數(shù)據(jù)源模式商膊,點擊生成代碼即可
生成代碼展示
多數(shù)據(jù)源模式下會在 config 包下生成多數(shù)據(jù)源相關(guān)的配置類及切面伏伐,如果大家有個性化需求可以通過修改 DynamicDataSourceAspect 切面來實現(xiàn)動態(tài)切換邏輯,現(xiàn)有切換邏輯基本足夠晕拆。
多數(shù)據(jù)源其實還可以通過代碼分包的方式實現(xiàn)藐翎,這種方式實現(xiàn)起來易于理解:配置多個數(shù)據(jù)源,掃描不同的包实幕,創(chuàng)建屬于自己的 sqlSessionFactory 和 txManager(事務(wù)管理器)吝镣,在使用的時候可以通過調(diào)用不同包下的 mapper 來實現(xiàn)多數(shù)據(jù)源的效果,但是這種方式的弊端也較為明顯昆庇,分包稍有不慎便會出錯末贾,并且如果想要實現(xiàn)不同數(shù)據(jù)源下的事務(wù)一致性也較為麻煩,在同一個 service 方法中操作多個數(shù)據(jù)庫因此受限整吆。
動態(tài)多數(shù)據(jù)源則不會有以上問題拱撵,因此代碼生成器選擇了動態(tài)多數(shù)據(jù)源的生成模式辉川,利用 aop 實現(xiàn)數(shù)據(jù)源的動態(tài)切換,并且可以保證多庫操作事務(wù)一致性拴测,后面會詳細(xì)講解乓旗。
代碼運行效果
在 idea 中運行生成的代碼,啟動完畢登錄集索,點擊左側(cè)菜單查詢:
查看后臺日志屿愚,發(fā)現(xiàn)會切換不同的數(shù)據(jù)庫執(zhí)行sql:
下面以 springboot 為例,講一下多數(shù)據(jù)源內(nèi)部原理抄谐。
動態(tài)多數(shù)據(jù)源內(nèi)部原理及核心代碼
動態(tài)多數(shù)據(jù)源的內(nèi)部原理其實就是 aop渺鹦,只不過復(fù)雜的是 aop 的實現(xiàn)過程。
mybatis 為我們提供了一個抽象類 AbstractRoutingDataSource蛹含,通過繼承此類毅厚,重寫 determineCurrentLookupKey 方法可以根據(jù)返回值決定當(dāng)前使用哪個數(shù)據(jù)源,因此我們創(chuàng)建類 DynamicDataSource 繼承 AbstractRoutingDataSource 并重寫 determineCurrentLookupKey 方法:
/**
* 重寫數(shù)據(jù)源選擇方法(獲取當(dāng)前線程設(shè)置的數(shù)據(jù)源)
* @author zrx
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
}
}
先不忙著實現(xiàn)浦箱,如果想要正確匹配數(shù)據(jù)源吸耿,我們還需要向 DynamicDataSource 類中注冊數(shù)據(jù)源,所以需要先對數(shù)據(jù)源進(jìn)行配置酷窥,這里注冊兩個數(shù)據(jù)源 db1(mysql) 和 db2(oracle)咽安,我們使用枚舉值 DB1 和 DB2 作為數(shù)據(jù)源 db1 和 db2 的 key:
package mutitest.config.mutidatasource;
/**
* 數(shù)據(jù)源枚舉
* @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;
/**
* 數(shù)據(jù)源配置類
*
* @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();
//設(shè)置默認(rèn)數(shù)據(jù)源
dynamicDataSource.setDefaultTargetDataSource(db1);
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceType.DB1, db1);
dataSourceMap.put(DataSourceType.DB2, db2);
//向動態(tài)數(shù)據(jù)源中注冊所有數(shù)據(jù)源信息
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
//返回動態(tài)數(shù)據(jù)源的事務(wù)管理器
return new DataSourceTransactionManager(dataSource);
}
}
通過以上配置,我們成功向 DynamicDataSource 中注冊了 db1 和 db2蓬推,如何才能獲取當(dāng)前程序運行中的數(shù)據(jù)源呢妆棒?這就需要我們用到 ThreadLocal,ThreadLocal 可以向當(dāng)前線程中 set 和 get 值并且不受其他線程影響沸伏,而我們服務(wù)器的每一個請求都由一個工作線程來處理(nio 模式也是一個請求一個工作線程處理糕珊,只是在接收請求的時候使用了 io 多路復(fù)用),所以可以使用 ThreadLocal 存儲當(dāng)前工作線程的數(shù)據(jù)源毅糟,ThreadLocal 在很多開源框架中都有使用红选,主要用于線程隔離。
創(chuàng)建 DynamicDataSourceHolder 類姆另,存儲當(dāng)前線程中的數(shù)據(jù)源:
package mutitest.config.mutidatasource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 數(shù)據(jù)源選擇器
*
* @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);
}
}
然后喇肋,實現(xiàn) determineCurrentLookupKey 方法,一行代碼即可:
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getType();
}
最后一步迹辐,我們要實現(xiàn)數(shù)據(jù)源的動態(tài)切換蝶防,則需要自己實現(xiàn)一個數(shù)據(jù)源動態(tài)切面,改變當(dāng)前線程中的數(shù)據(jù)源明吩,我們可以使用注解來輔助實現(xiàn)间学,在切面中通過掃描方法上的注解來得知具體切換到哪個數(shù)據(jù)源。
創(chuàng)建 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;
/**
* 多數(shù)據(jù)源注解
* @author zrx
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DBType {
DataSourceType value() default DataSourceType.DB1;
}
創(chuàng)建數(shù)據(jù)源動態(tài)切面 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;
/**
* 動態(tài)數(shù)據(jù)源切面(order 必須要設(shè)置贺喝,否則事務(wù)的切面會優(yōu)先執(zhí)行菱鸥,數(shù)據(jù)源已經(jīng)設(shè)置完了,再設(shè)置就無效了)
* @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("指定數(shù)據(jù)源[{}]不存在躏鱼,使用默認(rèn)數(shù)據(jù)源-> {}", dbType.value(), joinPoint.getSignature());
} else {
logger.info("use datasource {} -> {}", dbType.value(), joinPoint.getSignature());
// 切換當(dāng)前線程的數(shù)據(jù)源
DynamicDataSourceHolder.setType(dbType.value());
}
}
@After("@annotation(dbType)")
public void restoreDataSource(JoinPoint joinPoint, DBType dbType) {
logger.info("use datasource {} -> {}", dbType.value(), joinPoint.getSignature());
//方法執(zhí)行完氮采,清空,防止內(nèi)存泄漏
DynamicDataSourceHolder.clearType();
}
}
在數(shù)據(jù)源切面上需要添加 @Order 注解染苛,值取1鹊漠,這是因為之前我們配置了動態(tài)數(shù)據(jù)源事務(wù),spring 會因此生成事務(wù)代理并且會優(yōu)先于切面執(zhí)行茶行,事務(wù)代理一旦生成躯概,數(shù)據(jù)源便被固定,這樣我們在切面中切換數(shù)據(jù)源就會無效畔师,所以切面邏輯需要在事務(wù)代理之前執(zhí)行才可生效娶靡。
至此,動態(tài)多數(shù)據(jù)源基本實現(xiàn)完畢看锉!
事務(wù)一致性問題
使用動態(tài)多數(shù)據(jù)源的同時姿锭,也要注意保證事務(wù)一致性,大家可能遇到這種情況伯铣,傳統(tǒng)單數(shù)據(jù)源應(yīng)用中呻此,同一個 service ,在沒有開啟事務(wù)的方法里調(diào)用開啟事務(wù)的方法會導(dǎo)致事務(wù)失效腔寡,這是因為 spring 只會對相同的 service 代理一次焚鲜,否則如果在沒有開啟事務(wù)的方法中再次開啟自身代理會導(dǎo)致循環(huán)依賴問題出現(xiàn),類似 “無限套娃”:自己代理的方法調(diào)用自己代理的另一個方法放前,并且另一個方法還需要自己的代理忿磅。解決此類問題的方法很簡單,讓調(diào)用方開啟事務(wù)即可犀斋,多數(shù)據(jù)源模式中同樣適用贝乎。
除此之外,多數(shù)據(jù)源模式中還存在如下場景:serviceA 中的 A 和 B 方法都開啟了事務(wù)叽粹,但操作的是不同的數(shù)據(jù)庫(ip不同)览效,這個時候 A 調(diào)用 B,使用的是 A 的代理虫几,對 B 不適用锤灿,便會報錯,對此我們可以把 B 方法移入另一個 serviceB 中辆脸,在 serviceA 中注入 serviceB 但校,在 A 方法中使用 serviceB 調(diào)用 B 方法,這樣執(zhí)行到 B 方法的時候使用的便是 serviceB 的代理啡氢,看起來沒有問題状囱,但還有一點遺漏术裸,那就是事務(wù)的傳播行為。
我們都知道亭枷,Spring 中默認(rèn)的事務(wù)傳播行為是 required:如果需要開啟事務(wù)袭艺,則開啟事務(wù),如果已經(jīng)開啟事務(wù)叨粘,則加入當(dāng)前事務(wù)猾编。上文中,執(zhí)行 B 方法的時候雖然使用的是 serviceB 的代理升敲,但是由于其事務(wù)傳播行為是 required答倡,A 方法執(zhí)行的時候已經(jīng)開啟了事務(wù),所以導(dǎo)致 B 方法加入到了 A 方法的事務(wù)中驴党,但 A 和 B 屬于兩個不同的數(shù)據(jù)庫瘪撇,使用相同的事務(wù)管理器必然會出現(xiàn)問題。為了解決此問題港庄,我們可以把事務(wù)傳播行為改為 required_new:如果需要開啟事務(wù)设江,則開啟事務(wù),并且總是開啟新的事務(wù)攘轩。這樣執(zhí)行 B 方法的時候會開啟新的事務(wù)叉存,使用的便是 B 所在數(shù)據(jù)庫的事務(wù)管理器,B 方法也就可以正常執(zhí)行了度帮,并且如果 B 出現(xiàn)異常歼捏,如果 A 不主動捕獲,則 A笨篷,B 都會回滾瞳秽。
也許有人會問,單數(shù)據(jù)源模式下使用 required 為什么不會有上述問題呢率翅,因為單數(shù)據(jù)源模式下使用的是同一個數(shù)據(jù)庫练俐,在事務(wù)執(zhí)行過程中,當(dāng)前事務(wù)是共享且通用的冕臭,所以沒問題腺晾。除此之外,使用 required 不必頻繁重開事務(wù)辜贵,也一定程度上提升了系統(tǒng)性能悯蝉,多數(shù)據(jù)源模式下由于不同數(shù)據(jù)庫之間事務(wù)是完全隔離的,所以才需要使用 required_new 重開事務(wù)托慨,當(dāng)然鼻由,也需要根據(jù)業(yè)務(wù)具體場景具體分析,這里討論的只是較為通用的情況。
代碼生成器多數(shù)據(jù)源模式下使用的事務(wù)傳播行為正是 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;
/**
* 全局事務(wù)支持
*
* @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();
//可以根據(jù)業(yè)務(wù)需要自行添加需要被事務(wù)代理的方法
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());
}
}
到此為止蔼紧,我們才算實現(xiàn)了一個完整的動態(tài)多數(shù)據(jù)源功能,可見是有許多技術(shù)細(xì)節(jié)潛藏在里面的狠轻,朋友們可以使用代碼生成器生成多數(shù)據(jù)源模式下的代碼自行運行體會歉井。
結(jié)語
本文到這里就結(jié)束了,寫這個多數(shù)據(jù)源生成功能其實也算花了一番心思哈误,正著寫代碼容易,反過來生成是真不容易躏嚎,并且由于最開始做的時候沒有考慮到多數(shù)據(jù)源的情況蜜自,導(dǎo)致最開始的設(shè)計全都是針對單個數(shù)據(jù)庫的,這次強行在外面包了一層卢佣,總歸是實現(xiàn)了重荠,在這個過程中,順便也復(fù)習(xí)了一下 Spring 的循環(huán)依賴虚茶,Bean 加載周期等老生常談的問題戈鲁,也算有所收獲。作為開發(fā)人員嘹叫,我們要多關(guān)注一些功能底層的東西婆殿,而不是簡單的 api 調(diào)用,這樣才能不斷突破瓶頸罩扇,取得成長婆芦。碼字不易,各位看官可以點贊喂饥,在看消约,星標(biāo)關(guān)注哦,我們下次再見员帮!
關(guān)注公眾號 螺旋編程極客
獲取代碼生成器最新動態(tài)或粮,同時第一時間解鎖更多精彩內(nèi)容!