1. 程式人生 > >實現SpringBoot的多資料來源配置

實現SpringBoot的多資料來源配置

【場景】

  1. 當業務資料量達到了一定程度,DBA 需要合理配置資料庫資源。即配置主庫的機器高配置,把核心高頻的資料放在主庫上;把次要的資料放在從庫,低配置。
    –(引自 https://www.cnblogs.com/Alandre/p/6611813.html 泥瓦匠BYSocket 大神部落格)
  2. 實現讀寫分離(詳見

https://www.cnblogs.com/surge/p/3582248.html


【實現步驟(以renren-security為例)】

  1. 在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 的資料來源介面。詳見

https://www.cnblogs.com/surge/p/3582248.html

在這裡插入圖片描述在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的多資料來源配置就完成了。

完整相關程式碼請見

https://gitee.com/renrenio/renren-security