實現SpringBoot的多資料來源配置
【場景】
- 當業務資料量達到了一定程度,DBA 需要合理配置資料庫資源。即配置主庫的機器高配置,把核心高頻的資料放在主庫上;把次要的資料放在從庫,低配置。
–(引自 https://www.cnblogs.com/Alandre/p/6611813.html 泥瓦匠BYSocket 大神部落格) - 實現讀寫分離(詳見
)
【實現步驟(以renren-security為例)】
- 在yml中配置多資料來源
注意此處的 first 和 second,後續配置資料來源的名稱要與此處名稱一致。
2.配置多資料來源的標誌註解@DataSource,其使用方法是在具體進行業務編碼的時候,通過@DataSource(name = “first”)來實現資料來源的切換,此處的 first 是在 yml 檔案中定義的資料庫名稱。
這裡重點介紹下SpringBoot的3個註解:
Java註解之 @Target、@Retention、@Documented簡介
另外補充2個註解:
@Inherited註解 功能:允許子類繼承父類中的註解。
@interface意思是宣告一個註解,方法名對應引數名,返回值型別對應引數型別。
3.編寫DynamicDataSource類,該類需要繼承AbstractRoutingDataSource,在Spring容器載入的時候,就註冊(設定)資料來源,其中,AbstractRoutingDataSource重寫了determineCurrentLookupKey()方法,該方法從ThreadLocal(當前執行緒)中獲取資料來源,同時也避免了多執行緒操作資料來源的時候互相干擾。(其中,各DataSource類的關係是: AbstractRoutingDataSource繼承了AbstractDataSource ,而AbstractDataSource 又是DataSource 的子類。DataSource 是javax.sql 的資料來源介面。詳見
)在getDataSource方法中,使用了ThreadLocal類的get()方法以獲取當前執行緒,該方法遍歷Map的方式十分優雅,MARK一下。當然,getDataSource()方法是public static修飾的,會在類載入的時候就執行。該類實現功能的路徑是:獲取當前執行緒–>獲取當前執行緒中的資料來源–>設定預設資料來源、設定所有可用資料來源、執行初始化。
4. 使用@Aspect編寫DataSourceAspect切面,環繞立體織入資料來源調配功能。此處@Aspect和@Component方法必須同時使用,否則切面會不起作用。
/*@AspectJ可以使用切點函式定義切點*/
@Aspect
/*和sping整合的時候必須要這個註解,否則sping容器解析不到該切面導致切面不能工作*/
@Component
public class DataSourceAspect implements Ordered {
protected Logger logger = LoggerFactory.getLogger(getClass());
/**
* @Pointcut 註解以及切面類方法對切點進行命名,配置spring的配置攔截規則
* 當前執行方法上持有註解 io.renren.datasources.annotation.DataSource將被匹配
*/
@Pointcut("@annotation(io.renren.datasources.annotation.DataSource)")
public void dataSourcePointCut() {/*切面名稱是 dataSourcePointCut*/
}
/**
* 環繞 dataSourcePointCut()方法,在使用 @DataSource 註解的時候,會觸發資料來源設定方法
* @param point
* @return
* @throws Throwable
*/
@Around("dataSourcePointCut()")
/**
* ProceedingJoinPoint:aspectJ切面通過ProceedingJoinPoint獲取當前執行的方法,並且使用proceed()方法來執行目標方法:
*/
public Object around(ProceedingJoinPoint point) throws Throwable {
/*
通過pjp物件獲取Signature物件,該物件封裝了連線點的資訊。
比如通過getDeclaringType獲取連線點所在類的 class物件*/
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
/*獲取 DataSource 中的註解*/
DataSource ds = method.getAnnotation(DataSource.class);
if(ds == null){
/*設定預設資料來源為DataSourceNames.FIRST*/
DynamicDataSource.setDataSource(DataSourceNames.FIRST);
logger.debug("set datasource is " + DataSourceNames.FIRST);
}else {
DynamicDataSource.setDataSource(ds.name());
logger.debug("set datasource is " + ds.name());
}
try {
//使用proceed()方法來執行目標方法
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
logger.debug("clean datasource");
}
}
@Override
public int getOrder() {
return 1;
}
}
其中,這一塊程式碼很關鍵,在沒有考慮呼叫clearDataSource()方法之前,在測試中發現,當涉及事務處理的時候,比如,資料來源A執行了查詢操作,緊接著資料來源B執行寫入操作,會發現,錯了,資料被寫入了A資料來源。那麼,怎麼解決呢。這就需要呼叫一次clearDataSource()方法,因為ThreadLocal存在記憶體洩漏的問題。所謂記憶體洩漏,就是Tomcat在載入的時候會開啟執行緒池,第一個涉及資料庫的操作結束後,如果沒有及時將ThreadLocal中的DataSource清掉,那麼,在下一次的事務操作中,當前的DataSource仍然存在,就會導致混亂。
5.配置資料來源,即在Spring容器中裝配資料來源資訊。
@Configuration
public class DynamicDataSourceConfig {
/**
* @ConfigurationProperties主要作用:就是繫結application.properties中的屬性
*/
@Bean(name = "firstDataSource")
@ConfigurationProperties("spring.datasource.druid.first")
public DataSource firstDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "secondDataSource")
@ConfigurationProperties("spring.datasource.druid.second")
public DataSource secondDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "firstDataSourceTransactionManager")
public DataSourceTransactionManager firstDataSourceTransactionManager(@Qualifier("firstDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "secondDataSourceTransactionManager")
public DataSourceTransactionManager secondDataSourceTransactionManager(@Qualifier("secondDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
@Primary
public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
return new DynamicDataSource(firstDataSource, targetDataSources);
}
}
6.測試一下
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicDataSourceTest {
@Autowired
private DataSourceTestService dataSourceTestService;
@Test
public void test(){
//資料來源1
SysUserEntity user1 = dataSourceTestService.queryUser(1L);
System.out.println(ToStringBuilder.reflectionToString(user1));
//資料來源2
SysUserEntity user2 = dataSourceTestService.queryUser2(1L);
System.out.println(ToStringBuilder.reflectionToString(user2));
//資料來源1
SysUserEntity user3 = dataSourceTestService.queryUser(1L);
System.out.println(ToStringBuilder.reflectionToString(user3));
}
}
Service層呼叫@DataSource(name=“DataSourceNames.SECOND”)
至此,SpringBoot的多資料來源配置就完成了。
完整相關程式碼請見