亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

上篇介紹了數(shù)據(jù)源基礎(chǔ),并實(shí)現(xiàn)了基于兩套DataSource,兩套mybatis配置的多數(shù)據(jù)源,從基礎(chǔ)知識(shí)層面闡述了多數(shù)據(jù)源的實(shí)現(xiàn)思路。不了解的同學(xué)請(qǐng)戳→同學(xué),你的多數(shù)據(jù)源事務(wù)失效了!

正如文末回顧所講,這種方式的多數(shù)據(jù)源對(duì)代碼侵入性很強(qiáng),每個(gè)組件都要寫兩套,不適合大規(guī)模線上實(shí)踐。

對(duì)于多數(shù)據(jù)源需求,Spring早在 2007 年就注意到并且給出了解決方案,原文見:
dynamic-datasource-routing

Spring提供了一個(gè)AbstractRoutingDataSource類,用來實(shí)現(xiàn)對(duì)多個(gè)DataSource的按需路由,本文介紹的就是基于此方式實(shí)現(xiàn)的多數(shù)據(jù)源實(shí)踐。

一、什么是AbstractRoutingDataSource

先看類上的注釋:

Abstract {@link JAVAx.sql.DataSource} implementation that routes {@link #getConnection()} calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.

課代表翻譯:這是一個(gè)抽象類,可以通過一個(gè)lookup key,把對(duì)getConnection()方法的調(diào)用,路由到目標(biāo)DataSource。后者(指lookup key)通常是由和線程綁定的上下文決定的。

這段注釋可謂字字珠璣,沒有一句廢話。下文結(jié)合主要代碼解釋其含義。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    //目標(biāo) DataSource Map,可以裝很多個(gè) DataSource
    @Nullable
    private Map<Object, Object> targetDataSources;
    
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;

    //Bean初始化時(shí),將 targetDataSources 遍歷并解析后放入 resolvedDataSources
    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    /**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
     //根據(jù) #determineCurrentLookupKey()返回的lookup key 去解析好的數(shù)據(jù)源 Map 里取相應(yīng)的數(shù)據(jù)源
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        // 當(dāng)前 lookupKey 的值由用戶自己實(shí)現(xiàn)↓
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }
    
    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * <p>Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    // 該方法用來決定lookup key,通常用線程綁定的上下文來實(shí)現(xiàn)
    @Nullable
    protected abstract Object determineCurrentLookupKey();
    
    // 省略其余代碼...

}

首先看類圖

Spring是如何支持多數(shù)據(jù)源的

AbstractRoutingDataSource-uml

是個(gè)DataSource,并且實(shí)現(xiàn)了InitializingBean,說明有Bean的初始化操作。

其次看實(shí)例變量

private Map<Object, Object> targetDataSources;和private Map<Object, DataSource> resolvedDataSources;其實(shí)是一回事,后者是經(jīng)過對(duì)前者的解析得來的,本質(zhì)就是用來存儲(chǔ)多個(gè) DataSource實(shí)例的 Map。

最后看核心方法

使用DataSource,本質(zhì)就是調(diào)用其getConnection()方法獲得連接,從而進(jìn)行數(shù)據(jù)庫操作。

AbstractRoutingDataSource#getConnection()方法首先調(diào)用determineTargetDataSource(),決定使用哪個(gè)目標(biāo)數(shù)據(jù)源,并使用該數(shù)據(jù)源的getConnection()連接數(shù)據(jù)庫:

@Override
public Connection getConnection() throws SQLException {
   return determineTargetDataSource().getConnection();
}
protected DataSource determineTargetDataSource() {
   Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
   // 這里使用的 lookupKey 就能決定返回的數(shù)據(jù)源是哪個(gè)
   Object lookupKey = determineCurrentLookupKey();
   DataSource dataSource = this.resolvedDataSources.get(lookupKey);
   if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
   }
   if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
   }
   return dataSource;
}

所以重點(diǎn)就是determineCurrentLookupKey()方法,該方法是抽象方法,由用戶自己實(shí)現(xiàn),通過改變其返回值,控制返回不同的數(shù)據(jù)源。用表格表示如下:

LOOKUPKEY

DATASOURCE

first

firstDataSource

second

secondDataSource

如何實(shí)現(xiàn)這個(gè)方法呢?結(jié)合Spring在注釋里給的提示:

后者(指lookup key)通常是由和線程綁定的上下文決定的。

應(yīng)該能聯(lián)想到ThreadLocal了吧!ThreadLocal可以維護(hù)一個(gè)與當(dāng)前線程綁定的變量,充當(dāng)這個(gè)線程的上下文。

二、實(shí)現(xiàn)

設(shè)計(jì)yaml文件外部化配置多個(gè)數(shù)據(jù)源

spring:
  datasource:
    first:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:mem:db1
      username: sa
      password:
    second:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:mem:db2
      username: sa
      password:

創(chuàng)建lookupKey的上下文持有類:

/**
 * 數(shù)據(jù)源 key 上下文
 * 通過控制 ThreadLocal變量 LOOKUP_KEY_HOLDER 的值用于控制數(shù)據(jù)源切換
 * @see RoutingDataSource
 * @author :Java課代表
 */
public class RoutingDataSourceContext {

    private static final ThreadLocal<String> LOOKUP_KEY_HOLDER = new ThreadLocal<>();

    public static void setRoutingKey(String routingKey) {
        LOOKUP_KEY_HOLDER.set(routingKey);
    }

    public static String getRoutingKey() {
        String key = LOOKUP_KEY_HOLDER.get();
        // 默認(rèn)返回 key 為 first 的數(shù)據(jù)源
        return key == null ? "first" : key;
    }

    public static void reset() {
        LOOKUP_KEY_HOLDER.remove();
    }
}

實(shí)現(xiàn)AbstractRoutingDataSource:

/**
 * 支持動(dòng)態(tài)切換的數(shù)據(jù)源
 * 通過重寫 determineCurrentLookupKey 實(shí)現(xiàn)數(shù)據(jù)源切換
 * @author :Java課代表
 */
public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getRoutingKey();
    }

}

給我們的RoutingDataSource初始化上多個(gè)數(shù)據(jù)源:

/**
 * 數(shù)據(jù)源配置
 * 把多個(gè)數(shù)據(jù)源,裝配到一個(gè) RoutingDataSource 里
 * @author :Java課代表
 */
@Configuration
public class RoutingDataSourcesConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.first")
    public DataSource firstDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.second")
    public DataSource secondDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean
    public RoutingDataSource routingDataSource() {
        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(firstDataSource());
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("first", firstDataSource());
        dataSourceMap.put("second", secondDataSource());
        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }

}

演示一下手工切換的代碼:

public void init() {
    // 手工切換為數(shù)據(jù)源 first,初始化表
    RoutingDataSourceContext.setRoutingKey("first");
    createTableUser();
    RoutingDataSourceContext.reset();

    // 手工切換為數(shù)據(jù)源 second,初始化表
    RoutingDataSourceContext.setRoutingKey("second");
    createTableUser();
    RoutingDataSourceContext.reset();

}

這樣就實(shí)現(xiàn)了最基本的多數(shù)據(jù)源切換了。

不難發(fā)現(xiàn),切換工作很明顯可以抽成一個(gè)切面,我們可以優(yōu)化一下,利用注解標(biāo)明切點(diǎn),哪里需要切哪里。

三、引入AOP

自定義注解

/**
 * @author :Java課代表
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WithDataSource {
    String value() default "";
}

創(chuàng)建切面

@Aspect
@Component
// 指定優(yōu)先級(jí)高于@Transactional的默認(rèn)優(yōu)先級(jí)
// 從而保證先切換數(shù)據(jù)源再進(jìn)行事務(wù)操作
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DataSourceAspect {

    @Around("@annotation(withDataSource)")
    public Object switchDataSource(ProceedingJoinPoint pjp, WithDataSource withDataSource) throws Throwable {

        // 1.獲取 @WithDataSource 注解中指定的數(shù)據(jù)源
        String routingKey = withDataSource.value();
        // 2.設(shè)置數(shù)據(jù)源上下文
        RoutingDataSourceContext.setRoutingKey(routingKey);
        // 3.使用設(shè)定好的數(shù)據(jù)源處理業(yè)務(wù)
        try {
            return pjp.proceed();
        } finally {
            // 4.清空數(shù)據(jù)源上下文
            RoutingDataSourceContext.reset();
        }
    }
}

有了注解和切面,使用起來就方便多了:

// 注解標(biāo)明使用"second"數(shù)據(jù)源
@WithDataSource("second")
public List<User> getAllUsersFromSecond() {
    List<User> users = userService.selectAll();
    return users;
}

關(guān)于切面有兩個(gè)細(xì)節(jié)需要注意:

  1. 需要指定優(yōu)先級(jí)高于聲明式事務(wù)
  2. 原因:聲明式事務(wù)事務(wù)的本質(zhì)也是 AOP,其只對(duì)開啟時(shí)使用的數(shù)據(jù)源生效,所以一定要在切換到指定數(shù)據(jù)源之后再開啟,聲明式事務(wù)默認(rèn)的優(yōu)先級(jí)是最低級(jí),這里只需要設(shè)定自定義的數(shù)據(jù)源切面的優(yōu)先級(jí)比它高即可。
  3. 業(yè)務(wù)執(zhí)行完之后一定要清空上下文
  4. 原因:假設(shè)方法 A 使用@WithDataSource("second")指定走"second"數(shù)據(jù)源,緊跟著方法 B 不寫注解,期望走默認(rèn)的first數(shù)據(jù)源。但由于方法A放入上下文的lookupKey此時(shí)還是"second"并未刪除,所以導(dǎo)致方法 B 執(zhí)行的數(shù)據(jù)源與期望不符。

四、回顧

至此,基于AbstractRoutingDataSource+AOP的多數(shù)據(jù)源就實(shí)現(xiàn)好了。

在配置DataSource 這個(gè)Bean的時(shí)候,用的是自定義的RoutingDataSource,并且標(biāo)記為 @Primary。這樣就可以讓
mybatis-spring-boot-starter使用RoutingDataSource幫我們自動(dòng)配置好mybatis,比搞兩套DataSource+兩套Mybatis配置的方案簡(jiǎn)單多了。

文中相關(guān)代碼已上傳課代表的github

特別說明:

樣例中為了減少代碼層級(jí),讓展示更直觀,在 controller 層寫了事務(wù)注解,實(shí)際開發(fā)中可別這么干,controller 層的任務(wù)是綁定、校驗(yàn)參數(shù),封裝返回結(jié)果,盡量不要在里面寫業(yè)務(wù)!

五、優(yōu)化

對(duì)于一般的多數(shù)據(jù)源使用場(chǎng)景,本文方案已足夠覆蓋,可以實(shí)現(xiàn)靈活切換。

但還是存在如下不足:

  • 每個(gè)應(yīng)用使用時(shí)都要新增相關(guān)類,大量重復(fù)代碼
  • 修改或新增功能時(shí),所有相關(guān)應(yīng)用都得改
  • 功能不夠強(qiáng)悍,沒有高級(jí)功能,比如讀寫分離場(chǎng)景下的讀多個(gè)從庫負(fù)載均衡

其實(shí)把這些代碼封裝到一個(gè)starter里面,高級(jí)功能慢慢擴(kuò)展就可以。

好在開源世界早就有現(xiàn)成工具可用了,開發(fā)mybatis-plus的"baomidou"團(tuán)隊(duì)在其生態(tài)中開源了一個(gè)多數(shù)據(jù)源框架 Dynamic-Datasource,底層原理就是AbstractRoutingDataSource,增加了更多強(qiáng)悍的擴(kuò)展功能,下篇介紹其使用。

分享到:
標(biāo)簽:Spring
用戶無頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定