Springcloud + Mybatisが複数のデータソースを使用する4つの方法

メンバーシップセンターとミドルウェアシステムを開発しているときに、複数のデータソース構成の問題に何度も遭遇しました。主に、外注、パラメーター化スイッチング、注釈+ AOP、動的追加の4つの方法が使用されました。これが要約です。経験と踏み込んだ落とし穴を共有してください。

外注方法

データソース構成ファイル

ymlで、2つのデータソースを構成します。IDはmasterとs1です。

spring:
  datasource:
    master:
      jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db1?.........
      username: xxx
      password: xxx
      driverClassName: com.mysql.cj.jdbc.Driver
    s1:
      jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db2?........
      username: xxx
      password: xxx
      driverClassName: com.mysql.cj.jdbc.Driver

データソース構成クラス

マスターデータソース構成クラス

注意点:

@Primaryアノテーションを使用してデフォルトのデータソースを指定する必要があります。そうしないと、Springはどちらがプライマリデータソースであるかを認識しません。

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.xxx.xxx.mapper.master", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {    //默认数据源
    @Bean(name = "masterDataSource")
    @Primary    @ConfigurationProperties(prefix = "spring.datasource.master")
    public HikariDataSource masterDataSource() {        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }    @Bean(name = "masterSqlSessionFactory")
    @Primary    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource datasource, PaginationInterceptor paginationInterceptor)
            throws Exception {        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();        bean.setDataSource(datasource);        bean.setMapperLocations(                // 设置mybatis的xml所在位置
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/master/**/**.xml"));
        bean.setPlugins(new Interceptor[]{paginationInterceptor});
        return bean.getObject();
    }    @Bean(name = "masterSqlSessionTemplate")
    @Primary    public SqlSessionTemplate masterSqlSessionTemplate(            @Qualifier("masterSqlSessionFactory") SqlSessionFactory sessionfactory) {
        return new SqlSessionTemplate(sessionfactory);
    }}

s1データソース構成クラス

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.xxx.xxx.mapper.s1", sqlSessionFactoryRef = "s1SqlSessionFactory")
public class S1DataSourceConfig {
    @Bean(name = "s1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.s1")
    public HikariDataSource s1DataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }    @Bean(name = "s1SqlSessionFactory")
    public SqlSessionFactory s1SqlSessionFactory(@Qualifier("s1DataSource") DataSource datasource
            , PaginationInterceptor paginationInterceptor)            throws Exception {        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();        bean.setDataSource(datasource);        bean.setMapperLocations(                // 设置mybatis的xml所在位置
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/s1/**/**.xml"));
        bean.setPlugins(new Interceptor[]{paginationInterceptor});
        return bean.getObject();
    }
    @Bean(name = "s1SqlSessionTemplate")
    public SqlSessionTemplate s1SqlSessionTemplate(
            @Qualifier("s1SqlSessionFactory") SqlSessionFactory sessionfactory) {
        return new SqlSessionTemplate(sessionfactory);
    }
}

使用する

マッパーインターフェースとxmlファイルは、さまざまなデータソースに従ってパッケージ化する必要があることがわかります。データベースを操作するときは、必要に応じてdaoレイヤーをサービスクラスに挿入します。

特性分析

利点

実装は簡単で、データソース構成ファイルと構成クラス、マッパーインターフェイス、xmlファイルを作成するだけで、サブパッケージに注意を払うことができます。

不利益

もちろん、後でデータソースを追加または削除する場合は、データソース構成ファイルだけでなく、構成クラスも変更する必要があります。

たとえば、データソースを追加するには、データソースの新しい構成クラスを記述する必要があります。また、「ホットプラグ」効果を実現せずに、新しいマッパーインターフェイスパッケージやxmlパッケージなどを作成することも検討する必要があります。

パラメータ化された切り替え方法

思想

データソースのパラメーター化された切り替えは、ビジネス側が現在のビジネスパラメーターに基づいて異なるデータソースに動的に切り替える必要があることを意味します。

これは下請けのアイデアとは異なります。外注の前提は、コードを作成するときに、現在使用する必要のあるデータソースが既にわかっており、パラメーター化されたスイッチングデータソースがビジネスパラメーターに基づいて使用するデータソースを決定する必要があることです。

たとえば、リクエストパラメータのuserType値が1の場合はデータソースslave1に切り替える必要があり、リクエストパラメータのuserType値が2の場合はデータソースslave2に切り替える必要があります。

/**伪代码**/
int userType = reqUser.getType();
if (userType == 1){
    //切换到数据源slave1
    //数据库操作
} else if(userType == 2){
    //切换到数据源slave2
    //数据库操作
}

デザインのアイデア

データソース登録

データソース構成クラスがデータソースを作成すると、yml構成ファイルからすべてのデータソース構成を読み取り、各データソースを自動的に作成し、それをBeanファクトリとAbstractRoutingDatasourceに登録し(これについては後で説明します)、デフォルトのデータソースマスターを返します。

Springcloud + Mybatisが複数のデータソースを使用する4つの方法

 

データソースの切り替え

(1)要求はスレッドプールを介して処理され、各要求は1つのスレッドを占有するため、データソースの切り替え時に各スレッドが互いに影響を与えることはありません。

(2)ビジネス・パラメーターに応じて切り替える必要のあるデータ・ソースIDを取得し、IDに従ってデータ・ソースBeanをデータ・ソース・バッファー・プールから取得します。

(3)現在のスレッドデータソースキーを生成します。

(4)キーをthreadLocalに設定します。

(5)キーとデータソースBeanをデータソースバッファプールに入れます。

(6)マッパーメソッドを実行する前に、springは、decimateCurrentLookupKeyメソッドを呼び出してキーを取得し、データソースバッファープールに移動して、キーに基づいてデータソースをフェッチし、次にgetConnectionを使用してデータソース接続を取得します。

(7)このデータソースを使用してデータベース操作を実行します。

(8)現在のスレッドデータソースを解放します。

Springcloud + Mybatisが複数のデータソースを使用する4つの方法

 

AbstractRoutingDataSourceソースコード分析

Springは、データソースを動的に切り替えるためのキーであるAbstractRoutingDataSource抽象クラスを提供します。

DataSourceインターフェースを実装するこのクラスのクラス図を見てみましょう。

Springcloud + Mybatisが複数のデータソースを使用する4つの方法

 

まずgetTargetメソッドを呼び出してデータソースを取得し、次にデータベース接続を取得するgetConnectionメソッドのロジックを確認します。使用するデータソースを決定するのはここだと簡単に推測できます。

Springcloud + Mybatisが複数のデータソースを使用する4つの方法

 

preventTargetDataSourceメソッドを入力すると、最初にdefineCurrentLookupKeyを呼び出してlookupKeyを取得し、次にこのキーに基づいてresolveDataSourcesで対応するデータソースを見つけます。

Springcloud + Mybatisが複数のデータソースを使用する4つの方法

 

このクラスで定義されているいくつかのオブジェクトを見てみましょう。DefaultTargetDataSourceはデフォルトのデータソースで、resolvedDataSourcesはすべてのマスターおよびスレーブデータソースを格納するマップオブジェクトです。

Springcloud + Mybatisが複数のデータソースを使用する4つの方法

 

したがって、キーはこのlookupKeyの取得ロジックであり、現在取得されているデータソースを決定し、getConnectionなどの一連の操作を実行します。preventCurrentLookupKeyはAbstractRoutingDataSourceクラスの抽象メソッドであり、その戻り値は、使用するデータソースdataSourceのキー値です。このキー値を使用すると、resolvedDataSource(これはマップであり、設定後に構成ファイルに格納されます)対応するDataSourceを取り出し、それが見つからない場合は、デフォルトのデータソースを使用してください。

したがって、AbstractRoutingDataSourceクラスを拡張し、determineCurrentLookupKey()メソッドを書き換えることにより、データソースを切り替えることができます。

コード

キーコードの実装は以下に掲載されています。

データソース構成ファイル

3つのデータソースがあり、プライマリデータソースはMySQL、2つのセカンダリデータソースはsqlserverです。

spring:
  datasource:
    master:
      jdbcUrl: jdbc:mysql://192.168.xx.xxx:xxx/db1?........
      username: xxx
      password: xxx
      driverClassName: com.mysql.cj.jdbc.Driver
    slave1:
      jdbcUrl: jdbc:sqlserver://192.168.xx.xxx:xxx;DatabaseName=db2
      username: xxx
      password: xxx
      driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
    slave2:
      jdbcUrl: jdbc:sqlserver://192.168.xx.xxx:xxx;DatabaseName=db3
      username: xxx
      password: xxx
      driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver

動的データソースを定義する

主にAbstractRoutingDataSourceを継承し、determineCurrentLookupKeyメソッドを実装します。

public class DynamicDataSource extends AbstractRoutingDataSource {
    /*存储所有数据源*/
    private Map<Object, Object> backupTargetDataSources;
    public Map<Object, Object> getBackupTargetDataSources() {
        return backupTargetDataSources;
    }    /*defaultDataSource为默认数据源*/
    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSource) {
        backupTargetDataSources = targetDataSource;        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(backupTargetDataSources);
        super.afterPropertiesSet();
    }    public void addDataSource(String key, DataSource dataSource) {
        this.backupTargetDataSources.put(key, dataSource);
        super.setTargetDataSources(this.backupTargetDataSources);
        super.afterPropertiesSet();
    }    /*返回当前线程的数据源的key*/
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getContextKey();
    }}

データソースキースレッド変数の保持を定義する

スレッドとスレッドのデータソースキーとの関係を保持するThreadLocal静的変数を定義します。データソースを切り替える場合は、まず自分でキーを生成し、このキーをthreadLocalスレッド変数に格納する必要があります。同時に、DynamicDataSourceオブジェクトのbackupTargetDataSourcesプロパティからデータソースオブジェクトを取得し、キーとデータソースオブジェクトを再接続する必要があります。 backupTargetDataSourcesに配置します。このように、springは、determineCurrentLookupKeyメソッドによって返されたキーに従って、backupTargetDataSourcesから設定したばかりのデータソースオブジェクトを取り出し、getConnectionなどの一連の操作を実行できます。

public class DynamicDataSourceContextHolder {
    /**
     * 存储线程和数据源key的映射关系
     */
    private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();
    /***
     * 设置当前线程数据源key
     */
    public static void setContextKey(String key) {
        DATASOURCE_CONTEXT_KEY_HOLDER.set(key);
    }    /***
     * 获取当前线程数据源key
     */
    public static String getContextKey() {
        String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();        return key == null ? DataSourceConstants.DS_KEY_MASTER : key;
    }    /***
     * 删除当前线程数据源key
     */
    public static void removeContextKey() {
        DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class);        String currentKey = DATASOURCE_CONTEXT_KEY_HOLDER.get();        if (StringUtils.isNotBlank(currentKey) && !"master".equals(currentKey)) {
            dynamicDataSource.getBackupTargetDataSources().remove(currentKey);        }        DATASOURCE_CONTEXT_KEY_HOLDER.remove();    }}

マルチデータソースの自動構成クラス

ここでは、yml構成ファイル内のすべてのデータソースの構成を読み取ることにより、データソースオブジェクトがデータソースごとに自動的に作成され、Beanファクトリに登録されます。同時に、これらのデータソースオブジェクトはAbstractRoutingDataSourceに設定されます。

このように、後でデータソースを追加または変更する必要がある場合、Java構成クラスを追加または変更する必要はありません。構成センターに移動して、ymlファイルを変更するだけです。

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.modules.xxx.mapper")
public class DynamicDataSourceConfig {
    @Autowired
    private BeanFactory beanFactory;
    @Autowired
    private DynamicDataSourceProperty dynamicDataSourceProperty;
    /**
     * 功能描述: <br>
     * 〈动态数据源bean 自动配置注册所有数据源〉
     *
     * @param
     * @return javax.sql.DataSource
     * @Author li.he
     * @Date 2020/6/4 16:47
     * @Modifier
     */
    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) beanFactory;        /*获取yml所有数据源配置*/
        Map<String, Object> datasource = dynamicDataSourceProperty.getDatasource();        Map<Object, Object> dataSourceMap = new HashMap<>(5);
        Optional.ofNullable(datasource).ifPresent(map -> {            for (Map.Entry<String, Object> entry : map.entrySet()) {
                //创建数据源对象
                HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build();
                String dataSourceId = entry.getKey();
                configeDataSource(entry, dataSource);
                /*bean工厂注册每个数据源bean*/
                listableBeanFactory.registerSingleton(dataSourceId, dataSource);
                dataSourceMap.put(dataSourceId, dataSource);
            }
        });
        //AbstractRoutingDataSource设置主从数据源
        return new DynamicDataSource(beanFactory.getBean("master", DataSource.class),          dataSourceMap);
    }
    private void configeDataSource(Map.Entry<String, Object> entry, HikariDataSource dataSource) {
        Map<String, Object> dataSourceConfig = (Map<String, Object>) entry.getValue();
        dataSource.setJdbcUrl(MapUtils.getString(dataSourceConfig, "jdbcUrl"));
        dataSource.setDriverClassName(MapUtils.getString(dataSourceConfig, "driverClassName"));
        dataSource.setUsername(MapUtils.getString(dataSourceConfig, "username"));
        dataSource.setPassword(MapUtils.getString(dataSourceConfig, "password"));
    }
}

データソース切り替えツール

スイッチングロジック:

(1)現在のスレッドデータソースキーを生成する

(2)業務状況に応じて、切り替えるデータソースIDを取得します。

(3)IDに従ってデータソースバッファープールからデータソースオブジェクトを取得し、それをbackupTargetDataSourcesバッファープールに再度追加します。

(4)threadLocalは、現在のスレッドに対応するデータソースキーを設定します。

(5)データベースの操作を実行する前に、springは、determineCurrentLookupKeyメソッドを呼び出してキーを取得し、データソースバッファープールに移動して、キーに従ってデータソースをフェッチし、次にgetConnectionを使用してデータソース接続を取得します。

(6)データソースを使用してデータベース操作を実行します。

(7)キャッシュを解放します。threadLocalは現在のスレッドデータソース情報をクリーンアップし、データソースバッファープールは現在のスレッドデータソースキーとデータソースオブジェクトをクリーンアップします。目的はメモリリークを防ぐことです。

@Slf4j
@Component
public class DataSourceUtil {
    @Autowired
    private DataSourceConfiger dataSourceConfiger;        /*根据业务条件切换数据源*/
    public void switchDataSource(String key, Predicate<? super Map<String, Object>> predicate) {
        try {
            //生成当前线程数据源key
            String newDsKey = System.currentTimeMillis() + "";
            List<Map<String, Object>> configValues = dataSourceConfiger.getConfigValues(key);
            Map<String, Object> db = configValues.stream().filter(predicate)
                    .findFirst().get();
            String id = MapUtils.getString(db, "id");
            //根据ID从数据源缓存池中获取数据源对象,并再次添加到backupTargetDataSources
            addDataSource(newDsKey, id);
            //设置当前线程对应的数据源key
            DynamicDataSourceContextHolder.setContextKey(newDsKey);
            log.info("当前线程数据源切换成功,当前数据源ID:{}", id);
        }
        catch (Exception e) {
            log.error("切换数据源失败,请检查数据源配置文件。key:{}, 条件:{}", key, predicate.toString());
            throw new ClientException("切换数据源失败,请检查数据源配置", e);
        }
    }
    
    /*将数据源添加至多数据源缓存池中*/
    public static void addDataSource(String key, String dataSourceId) {
        DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class);
        DataSource dataSource = (DataSource) dynamicDataSource.getBackupTargetDataSources().get(dataSourceId);
        dynamicDataSource.addDataSource(key, dataSource);
    }
}

使用する

public void doExecute(ReqTestParams reqTestParams){
    //构造条件
    Predicate<? super Map<String, Object>> predicate =.........;
    //切换数据源
    dataSourceUtil.switchDataSource("testKey", predicate);
    //数据库操作
    mapper.testQuery();    //清理缓存,避免内存泄漏
    DynamicDataSourceContextHolder.removeContextKey();}

各データソースを使用した後、removeContextKeyメソッドを呼び出して、メモリリークを回避するためにキャッシュをクリーンアップする必要があります。ここでは、AOPを使用して特定のメソッドをインターセプトし、ポスト通知を使用して実行メソッドプロキシのキャッシュクリーニングを実行することを検討できます。

@Aspect
@Component
@Slf4j
public class RequestHandleMethodAspect {
    @After("xxxxxxxxxxxxxxExecution表达式xxxxxxxxxxxxxxxxxx")
    public void afterRunning(JoinPoint joinPoint){
        String name = joinPoint.getSignature().toString();        long id = Thread.currentThread().getId();
        log.info("方法执行完毕,开始清空当前线程数据源,线程id:{},代理方法:{}",id,name);
        DynamicDataSourceContextHolder.removeContextKey();        log.info("当前线程数据源清空完毕,已返回至默认数据源:{}",id);
    }}

特性分析

(1)パラメータ化されたスイッチングデータソースモード。開始点は、実行時に使用するデータソースを決定するのに適した外注モードとは異なります。

(2)データソース操作を手動で切り替える必要がある。

(3)外注は不要で、マッパーとxmlパスを自由に定義できます。

(4)データソースを追加するには、Java構成クラスを変更する必要はありません。データソース構成ファイルを変更するだけです。

注釈方法

思想

このメソッドは、アノテーション+ AOPアイデアを使用して、データソースを切り替える必要があるメソッドのカスタムアノテーションをマークし、アノテーション属性でデータソースIDを指定し、AOPアスペクトを使用してアノテーションマークメソッドをインターセプトし、メソッドが実行される前に対応するデータソースに切り替えます。実行が終了したら、デフォルトのデータソースに切り替えます。

カスタムアスペクトの優先度は、@ Transactionalアノテーションの対応するアスペクトの優先度よりも高い必要があることに注意してください。

それ以外の場合、カスタムアノテーションと@Transactionalが同時に使用されると、@ Transactionalアスペクトが最初に実行されます。アスペクトがgetConnectionメソッドを呼び出すと、AbstractRoutingDataSource.determineCurrentLookupKeyメソッドが呼び出されます。このとき、デフォルトのデータソースマスターが取得されます。このとき、@ UsingDataSourceに対応するアスペクトに現在のスレッドのデータソースキーが設定されていても、後でデータソースを切り替えるためにpreventCurrentLookupKeyメソッドが呼び出されることはありません。

デザインのアイデア

データソース登録

同上。

データソースの切り替え

アスペクトを使用して、すべての@UsingDataSourceアノテーションメソッドをインターセプトし、dataSourceId属性に従って、メソッドが実行される前に対応するデータソースに切り替えます。メソッドが実行された後、キャッシュをクリアしてデフォルトのデータソースに切り替えます。

Springcloud + Mybatisが複数のデータソースを使用する4つの方法

 

コード

データソース構成ファイル

同上。

動的データソースを定義する

同上。

データソースキースレッド変数の保持を定義する

同上。

マルチデータソースの自動構成クラス

同上。

データソース切り替えツール

スイッチングロジック:

(1)現在のスレッドデータソースキーを生成する

(3)IDに従ってデータソースバッファープールからデータソースオブジェクトを取得し、それをbackupTargetDataSourcesバッファープールに再度追加します。

(4)threadLocalは、現在のスレッドに対応するデータソースキーを設定します。

(5)データベースの操作を実行する前に、springは、determineCurrentLookupKeyメソッドを呼び出してキーを取得し、データソースバッファープールに移動して、キーに従ってデータソースをフェッチし、次にgetConnectionを使用してデータソース接続を取得します。

(6)データソースを使用してデータベース操作を実行します。

(7)キャッシュを解放します。threadLocalは現在のスレッドデータソース情報をクリーンアップし、データソースバッファープールは現在のスレッドデータソースキーとデータソースオブジェクトをクリーンアップします。

public static void switchDataSource(String dataSourceId) {
    if (StringUtils.isBlank(dataSourceId)) {
        throw new ClientException("切换数据源失败,数据源ID不能为空");
    }    try {
        String threadDataSourceKey = UUID.randomUUID().toString();        DataSourceUtil.addDataSource(threadDataSourceKey, dataSourceId);        DynamicDataSourceContextHolder.setContextKey(threadDataSourceKey);    }    catch (Exception e) {
        log.error("切换数据源失败,未找到指定的数据源,请确保所指定的数据源ID已在配置文件中配置。dataSourceId:{}", dataSourceId);
        throw new ClientException("切换数据源失败,未找到指定的数据源,请确保所指定的数据源ID已在配置文件中配置。dataSourceId:" + dataSourceId, e);
    }}

カスタム注釈

カスタムアノテーションは、現在のメソッドで使用されるデータソースをマークし、デフォルトでメインデータソースになります。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UsingDataSource {
    String dataSourceId() default "master";
}

セクション

主に事前通知と事後通知を定義し、UsingDataSourceアノテーションマークのメソッドをインターセプトし、メソッドが実行される前にデータソースを切り替え、メソッドが実行された後にデータソースキャッシュを消去します。

マークする必要のあるアスペクトの優先度は、@ Transactionアノテーションの対応するアスペクトの優先度よりも高くなっています。それ以外の場合、カスタムアノテーションと@Transactionalが同時に使用されると、@ Transactionalアスペクトが最初に実行されます。アスペクトがgetConnectionメソッドを呼び出すと、AbstractRoutingDataSource.determineCurrentLookupKeyメソッドが呼び出されます。このとき、デフォルトのデータソースマスターが取得されます。このとき、@ UsingDataSourceに対応するアスペクトに現在のスレッドのデータソースキーが設定されていても、後でデータソースを切り替えるためにpreventCurrentLookupKeyメソッドが呼び出されることはありません。

@Aspect
@Component
@Slf4j
@Order(value = 1)
public class DynamicDataSourceAspect {    //拦截UsingDataSource注解标记的方法,方法执行前切换数据源
    @Before(value = "@annotation(usingDataSource)")
    public void before(JoinPoint joinPoint, UsingDataSource usingDataSource) {
        String dataSourceId = usingDataSource.dataSourceId();
        log.info("执行目标方法前开始切换数据源,目标方法:{}, dataSourceId:{}", joinPoint.getSignature().toString(), dataSourceId);
        try {
            DataSourceUtil.switchDataSource(dataSourceId);
        }
        catch (Exception e) {
            log.error("切换数据源失败!数据源可能未配置或不可用,数据源ID:{}", dataSourceId, e);
            throw new ClientException("切换数据源失败!数据源可能未配置或不可用,数据源ID:" + dataSourceId, e);
        }
        log.info("目标方法:{} , 已切换至数据源:{}", joinPoint.getSignature().toString(), dataSourceId);
    }
    //拦截UsingDataSource注解标记的方法,方法执行后清理数据源,防止内存泄漏
    @After(value = "@annotation(com.hosjoy.hbp.dts.common.annotation.UsingDataSource)")
    public void after(JoinPoint joinPoint) {
        log.info("目标方法执行完毕,执行清理,切换至默认数据源,目标方法:{}", joinPoint.getSignature().toString());
        try {
            DynamicDataSourceContextHolder.removeContextKey();
        }
        catch (Exception e) {
            log.error("清理数据源失败", e);
            throw new ClientException("清理数据源失败", e);
        }
        log.info("目标方法:{} , 数据源清理完毕,已返回默认数据源", joinPoint.getSignature().toString());
    }
}

使用する

@UsingDataSource(dataSourceId = "slave1")
@Transactional
public void test(){
    AddressPo po = new AddressPo();
    po.setMemberCode("asldgjlk");
    po.setName("lihe");
    po.setPhone("13544986666");
    po.setProvince("asdgjwlkgj");
    addressMapper.insert(po);    int i = 1 / 0;
}

動的加算方式(非常に便利)

ビジネスシナリオの説明

この種のビジネスシナリオはあまり一般的ではありませんが、誰かが間違いなくそれに遭遇しました。

プロジェクトで構成されているデフォルトのデータソースは1つだけで、新しいデータソースは、構成されている静的な複数のデータソースではなく、特定のランタイム中に動的に追加する必要があります。たとえば、サーバーにアクセスして、データソースの構成情報をリアルタイムで(ローカルに構成されていない)読み取り、データベース操作を実行する必要があります。

このビジネスシナリオでは、上記のデータソースが事前にymlファイルで構成されているため、上記の3つの方法は適用できません。

実現アイデア

手順6を除いて、前に記述されたコードを使用してそれを実現できます。

アイデアは:

(1)新しいデータソースを作成します。

(2)DynamicDataSourceは新しいデータソースを登録します。

(3)スイッチ:現在のスレッドデータソースキーを設定し、一時データソースを追加します。

(4)データベース操作(別のサービスで実装する必要があります。実装しないと、トランザクションを制御できません)。

(5)現在のスレッドデータソースキーをクリーンアップし、一時データソースをクリーンアップします。

(6)登録したばかりのデータソースをクリーンアップします。

(7)現時点では、デフォルトのデータソースに戻っています。

コード

コードは比較的粗雑ですが、テンプレートはおそらく実装の方法を表現するために、おそらくこのようになります。

サービスA:

public String testUsingNewDataSource(){
        DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean("dynamicDataSource", DynamicDataSource.class);
        try {            //模拟从服务器读取数据源信息
            //..........................
            //....................
            
            //创建新数据源
            HikariDataSource dataSource = (HikariDataSource)                   DataSourceBuilder.create().build();
            dataSource.setJdbcUrl("jdbc:mysql://192.168.xxx.xxx:xxxx/xxxxx?......");
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUsername("xxx");
            dataSource.setPassword("xxx");
            
            //DynamicDataSource注册新数据源
            dynamicDataSource.addDataSource("test_ds_id", dataSource);
            //设置当前线程数据源key、添加临时数据源
            DataSourceUtil.switchDataSource("test_ds_id");
            //数据库操作(必须在另一个service实现,否则无法控制事务)
            serviceB.testInsert();
        }
        finally {
            //清理当前线程数据源key
            DynamicDataSourceContextHolder.removeContextKey();
            //清理刚刚注册的数据源
            dynamicDataSource.removeDataSource("test_ds_id");
        }
        return "aa";
    }

サービスB:

@Transactional(rollbackFor = Exception.class)
    public void testInsert() {
        AddressPo po = new AddressPo();
        po.setMemberCode("555555555");
        po.setName("李郃");
        po.setPhone("16651694996");
        po.setProvince("江苏省");
        po.setCity("南京市");
        po.setArea("浦口区");
        po.setAddress("南京市浦口区宁六路219号");
        po.setDef(false);
        po.setCreateBy("23958");
        addressMapper.insert(po);        //测试事务回滚
        int i = 1 / 0;
    }

DynamicDataSource:removeDataSourceメソッドを追加して、新しく登録されたデータソースをクリーンアップします。

public class DynamicDataSource extends AbstractRoutingDataSource {
                .................            .................            .................    public void removeDataSource(String key){
        this.backupTargetDataSources.remove(key);
        super.setTargetDataSources(this.backupTargetDataSources);
        super.afterPropertiesSet();
    }                .................            .................            .................}

比較する4つの方法

Springcloud + Mybatisが複数のデータソースを使用する4つの方法

 

トランザクションの問題

上記のデータソース構成方法を使用して、単一のデータソーストランザクション制御を実現できます。

例えば、サービス方式では、CUDを実行するために複数のデータソースを操作する必要がある場合に、単一のデータソーストランザクション制御を実現できます。方法は次のとおりですが、トランザクション制御が必要なメソッドを別のサービスに個別に抽出して、単一のトランザクション方式のトランザクション制御を実現します。

ServiceA:

public void updateMuilty(){
     serviceB.updateDb1();     serviceB.updateDb2();}

ServiceB:

@UsingDataSource(dataSourceId = "slave1")
@Transactional
public void updateDb1(){
    //业务逻辑......
}
@UsingDataSource(dataSourceId = "slave2")
@Transactional
public void updateDb2(){
    //业务逻辑......
}

しかし、同じ方法で複数のデータソースのトランザクションを制御することはそれほど単純ではありません。これは分散トランザクションの範囲に属します。atominosオープンソースプロジェクトを使用して、JTA分散トランザクション処理またはAliのFescarフレームワークを実装することを検討できます。

分散トランザクション制御が関係するため、実装はより複雑になりますが、これはこの問題を提起するためだけのものです。

高品質の記事の推奨読書:

アリババの高度な面接質問(最初の問題、136のよくある質問、回答を含む)

https://blog.csdn.net/weixin_45132238/article/details/107251285

GitHub Biaoxing 20wの4つの低レベルインタビューガイド(コンピューターのボトムレイヤー+オペレーティングシステム+アルゴリズム)、インタビューのヘッドライン/ Tencentは正しいです!

https://blog.csdn.net/weixin_45132238/article/details/108640805

Alibaba内部アーキテクチャの戦闘:SpringBoot / SpringCloud / Docker / Nginx / distributed

https://blog.csdn.net/weixin_45132238/article/details/108666255

おすすめ

転載: blog.csdn.net/weixin_45132238/article/details/108735953