これが最もジャオなSpringBoot + Mybatis構成のマルチデータソースおよびトランザクションソリューションだと思います

序文

おそらく特定のビジネス要件のために、システムが複数のデータベースに接続する必要がある場合があり、これにより複数のデータソースの問題が発生します。

複数のデータソースの場合、通常は自動切り替えを行う必要があります。現時点では、トランザクションアノテーションのトランザクション無効問題と分散トランザクション問題が関係します。

マルチデータソースソリューションに関して、著者はインターネット上でいくつかの例を見てきましたが、それらのほとんどは間違った例であり、まったく機能しないか、トランザクションと互換性のある方法がありません。

今日は、これらの問題の根本原因とそれに対応する解決策を少しずつ分析します。

これが最もジャオなSpringBoot + Mybatis構成のマルチデータソースおよびトランザクションソリューションだと思います

 

1.複数のデータソース

プロットをスムーズに開発するために、シミュレートされたビジネスは注文を作成し、在庫を差し引くことです。

したがって、最初に注文テーブルと在庫テーブルを作成します。それらは2つのデータベースに配置されていることに注意してください。

CREATE TABLE `t_storage` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `t_order` (
  `id` bigint(16) NOT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  `amount` double(14,2) DEFAULT '0.00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

1.データベース接続

最初にYMLファイルを使用して両方のデータベースを構成します。

spring:
  datasource:
    ds1:
      jdbc_url: jdbc:mysql://127.0.0.1:3306/db1
      username: root
      password: root
    ds2:
      jdbc_url: jdbc:mysql://127.0.0.1:3306/db2
      username: root
      password: root

2.データソースを構成します

MybatisがSQLステートメントを実行するときは、最初に接続を取得する必要があることがわかっています。このとき、データソースへの接続を取得するためにSpringマネージャに渡されます。

Springにはルーティング機能を備えたデータソースがあり、検索キーを介してさまざまなデータソースを呼び出すことができます。これはAbstractRoutingDataSourceです。

public abstract class AbstractRoutingDataSource{
    //数据源的集合
    @Nullable
    private Map<Object, Object> targetDataSources;
    //默认的数据源
    @Nullable
    private Object defaultTargetDataSource;
	
    //返回当前的路由键,根据该值返回不同的数据源
    @Nullable
    protected abstract Object determineCurrentLookupKey();
    
    //确定一个数据源
    protected DataSource determineTargetDataSource() {
        //抽象方法 返回一个路由键
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.targetDataSources.get(lookupKey);
        return dataSource;
    }
}

ご覧のとおり、この抽象クラスの中核は、最初に複数のデータソースをMapコレクションに設定し、次にキーに基づいてさまざまなデータソースを取得することです。

次に、データソースの名前を返すdetermineCurrentLookupKeyメソッドを書き直すことができます。

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceType.DataBaseType dataBaseType = DataSourceType.getDataBaseType();
        return dataBaseType;
    }
}

次に、現在のスレッドのデータソースタイプを保存するためのツールクラスが必要です。

public class DataSourceType {

    public enum DataBaseType {
        ds1, ds2
    }
    // 使用ThreadLocal保证线程安全
    private static final ThreadLocal<DataBaseType> TYPE = new ThreadLocal<DataBaseType>();
    // 往当前线程里设置数据源类型
    public static void setDataBaseType(DataBaseType dataBaseType) {
        if (dataBaseType == null) {
            throw new NullPointerException();
        }
        TYPE.set(dataBaseType);
    }
    // 获取数据源类型
    public static DataBaseType getDataBaseType() {
        DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.ds1 : TYPE.get();
        return dataBaseType;
    }
}

これらすべてが完了した後でも、このデータソースをSpringコンテナに構成する必要があります。次の構成クラスは次のように機能します。

  • 複数のデータソースDataSource、ds1およびds2を作成します。
  • ds1およびds2データソースを動的データソースDynamicDataSourceに配置します。
  • DynamicDataSourceをSqlSessionFactoryに挿入します。
@Configuration
public class DataSourceConfig {

    /**
     * 创建多个数据源 ds1 和 ds2
     * 此处的Primary,是设置一个Bean的优先级
     * @return
     */
    @Primary
    @Bean(name = "ds1")
    @ConfigurationProperties(prefix = "spring.datasource.ds1")
    public DataSource getDateSource1() {
        return DataSourceBuilder.create().build();
    }
    @Bean(name = "ds2")
    @ConfigurationProperties(prefix = "spring.datasource.ds2")
    public DataSource getDateSource2() {
        return DataSourceBuilder.create().build();
    }


    /**
     * 将多个数据源注入到DynamicDataSource
     * @param dataSource1
     * @param dataSource2
     * @return
     */
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DataSource(@Qualifier("ds1") DataSource dataSource1,
                                        @Qualifier("ds2") DataSource dataSource2) {
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceType.DataBaseType.ds1, dataSource1);
        targetDataSource.put(DataSourceType.DataBaseType.ds2, dataSource2);
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSource);
        dataSource.setDefaultTargetDataSource(dataSource1);
        return dataSource;
    }
    
    
    /**
     * 将动态数据源注入到SqlSessionFactory
     * @param dynamicDataSource
     * @return
     * @throws Exception
     */
    @Bean(name = "SqlSessionFactory")
    public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
        bean.setTypeAliasesPackage("cn.youyouxunyin.multipledb2.entity");
        return bean.getObject();
    }
}

3.ルーティングキーを設定します

上記の構成が完了した後でも、システムのビジネスに関連するデータソースのキー値を動的に変更する方法を見つける必要があります。

たとえば、ここでは、注文を作成して在庫を差し引くための2つのマッパーインターフェースがあります。

public interface OrderMapper {
    void createOrder(Order order);
}
public interface StorageMapper {
    void decreaseStorage(Order order);
}

次に、アスペクトを作成できます。注文操作を実行する場合はデータソースds1に切り替え、在庫操作を実行する場合はデータソースds2に切り替えます。

@Component
@Aspect
public class DataSourceAop {
    @Before("execution(* cn.youyouxunyin.multipledb2.mapper.OrderMapper.*(..))")
    public void setDataSource1() {
        DataSourceType.setDataBaseType(DataSourceType.DataBaseType.ds1);
    }
    @Before("execution(* cn.youyouxunyin.multipledb2.mapper.StorageMapper.*(..))")
    public void setDataSource2() {
        DataSourceType.setDataBaseType(DataSourceType.DataBaseType.ds2);
    }
}

4.テスト

これで、Serviceメソッドを記述し、RESTインターフェースを介してテストできます。

public class OrderServiceImpl implements OrderService {
    @Override
    public void createOrder(Order order) {
        storageMapper.decreaseStorage(order);
        logger.info("库存已扣减,商品代码:{},购买数量:{}。创建订单中...",order.getCommodityCode(),order.getCount());
        orderMapper.createOrder(order);
    }
}

当然のことながら、ビジネスの実行後、2つのデータベースのテーブルが変更されました。

しかし、現時点では、これら2つの操作で原子性を確保する必要があると考えます。したがって、トランザクションに依存し、ServiceメソッドでTransactionalをマークする必要があります。

createOrderメソッドにTransactionalアノテーションを追加してからコードを実行すると、例外がスローされます。

### Cause: java.sql.SQLSyntaxErrorException: Table 'db2.t_order' doesn't exist
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: 
    Table 'db2.t_order' doesn't exist] with root cause

これは、Springのトランザクションが追加された場合、データソースを切り替えることができないことを意味します。ここで何が起こっているのですか?

2.トランザクションモード、データソースを切り替えられないのはなぜですか

理由を見つけるには、Springトランザクションが追加されているかどうかを分析して分析する必要がありますが、それは何をしますか?

Springの自動トランザクションはAOPに基づいていることがわかっています。トランザクションを含むメソッドを呼び出すと、インターセプターが入力されます。

public class TransactionInterceptor{
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //获取目标类
        Class<?> targetClass = AopUtils.getTargetClass(invocation.getThis());
        //事务调用
        return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }
}

1.トランザクションを作成します

その中で、最初に行うことはトランザクションを作成することです。

protected Object doGetTransaction() {
    //DataSource的事务对象
    DataSourceTransactionObject txObject = new DataSourceTransactionObject();
    //设置事务自动保存
    txObject.setSavepointAllowed(isNestedTransactionAllowed());
    //给事务对象设置ConnectionHolder
    ConnectionHolder conHolder = TransactionSynchronizationManager.getResource(obtainDataSource());
    txObject.setConnectionHolder(conHolder, false);
    return txObject;
}

このステップでは、ConnectionHolderプロパティをトランザクションオブジェクトに設定することに重点を置いていますが、現時点ではまだ空です。

2.トランザクションを開きます

次に、トランザクションを開始します。ここでは、主にThreadLocalを介してリソースと現在のトランザクションオブジェクトをバインドし、トランザクションステータスを設定します。

protected void doBegin(Object txObject, TransactionDefinition definition) {
    
    Connection con = null;
    //从数据源中获取一个连接
    Connection newCon = obtainDataSource().getConnection();
    //重新设置事务对象中的connectionHolder,此时已经引用了一个连接
    txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
    //将这个connectionHolder标记为与事务同步
    txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
    con = txObject.getConnectionHolder().getConnection();
    con.setAutoCommit(false);
    //激活事务活动状态
    txObject.getConnectionHolder().setTransactionActive(true);
    //将connection holder绑定到当前线程,通过threadlocal
    if (txObject.isNewConnectionHolder()) {
        TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
    }
    //事务管理器,激活事务同步状态
    TransactionSynchronizationManager.initSynchronization();
}

3.マッパーインターフェースを実行します

トランザクションが開かれた後、ターゲットクラスの実際のメソッドが実行されます。ここでは、Mybatisのプロキシオブジェクトの入力を開始します。ハハ、フレームワークはあらゆる種類のエージェントです。

MybatisはSQLを実行する前にSqlSessionオブジェクトを取得する必要があることがわかっています。

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
                PersistenceExceptionTranslator exceptionTranslator) {

    //从ThreadLocal中获取SqlSessionHolder,第一次获取不到为空
    SqlSessionHolder holder = TransactionSynchronizationManager.getResource(sessionFactory);
    
    //如果SqlSessionHolder为空,那也肯定获取不到SqlSession;
    //如果SqlSessionHolder不为空,直接通过它来拿到SqlSession
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
        return session;
    }
    //创建一个新的SqlSession
    session = sessionFactory.openSession(executorType);
    //如果当前线程的事务处于激活状态,就将SqlSessionHolder绑定到ThreadLocal
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
    return session;
}

SqlSessionを取得した後、SQLステートメントを実行する準備をするためにMybatisエグゼキューターを呼び出し始めました。もちろん、SQLを実行する前に、まず接続接続を取得する必要があります。

public Connection getConnection() throws SQLException {
    //通过数据源获取连接
    //比如我们配置了多数据源,此时还会正常切换
    if (this.connection == null) {
        openConnection();
    }
    return this.connection;
}

openConnectionメソッドを見てみましょう。その役割は、データソースから接続接続を取得することです。複数のデータソースを設定する場合、この時点で通常どおり切り替えることができます。トランザクションが追加された場合、データソースが切り替えられない理由は、2回目の呼び出しで、this.connection!= nullが最後の接続を返すためです。

これは、SqlSessionが2回目に取得されるときに、現在のスレッドがThreadLocalから取得されるため、接続接続が繰り返し取得されないためです。

これまでのところ、複数のデータソースの場合、Springトランザクションを追加すると、データソースを動的に切り替えることができない理由を理解する必要があります。

ここで、著者はインタビューの質問を挿入します。

  • Springはどのようにトランザクションを保証しますか?

つまり、複数のビジネスオペレーションを同じデータベース接続に配置し、一緒に送信またはロールバックします。

  • すべてを1つの接続で行う方法は?

ここでは、さまざまなThreadlLocalを使用して、データベースリソースと現在のトランザクションをバインドする方法を見つけます。

3、トランザクションモード、データソースの切り替えをサポートする方法

上記の理由を理解した後、データソースを動的に切り替えるためにそれをサポートする方法を見ていきます。

他の構成を変更せずに、2つの異なるsqlSessionFactoryを作成する必要があります。

@Bean(name = "sqlSessionFactory1")
public SqlSessionFactory sqlSessionFactory1(@Qualifier("ds1") DataSource dataSource){
    return createSqlSessionFactory(dataSource);
}

@Bean(name = "sqlSessionFactory2")
public SqlSessionFactory sqlSessionFactory2(@Qualifier("ds2") DataSource dataSource){
    return createSqlSessionFactory(dataSource);
}

次に、CustomSqlSessionTemplateをカスタマイズしてMybatisの元のsqlSessionTemplateを置き換え、上記で定義した2つのSqlSessionFactoryを挿入します。

@Bean(name = "sqlSessionTemplate")
public CustomSqlSessionTemplate sqlSessionTemplate(){
    Map<Object,SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();
    sqlSessionFactoryMap.put("ds1",factory1);
    sqlSessionFactoryMap.put("ds2",factory2);
    CustomSqlSessionTemplate customSqlSessionTemplate = new CustomSqlSessionTemplate(factory1);
    customSqlSessionTemplate.setTargetSqlSessionFactorys(sqlSessionFactoryMap);
    customSqlSessionTemplate.setDefaultTargetSqlSessionFactory(factory1);
    return customSqlSessionTemplate;
}

定義されたCustomSqlSessionTemplateでは、他のすべては同じであり、主にSqlSessionFactoryを取得する方法に依存します。

public class CustomSqlSessionTemplate extends SqlSessionTemplate {
    @Override
    public SqlSessionFactory getSqlSessionFactory() {
        //当前数据源的名称
        String currentDsName = DataSourceType.getDataBaseType().name();
        SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys.get(currentDsName);
        if (targetSqlSessionFactory != null) {
            return targetSqlSessionFactory;
        } else if (defaultTargetSqlSessionFactory != null) {
            return defaultTargetSqlSessionFactory;
        }
        return this.sqlSessionFactory;
    }
}

ここで重要なのは、さまざまなデータソースに応じてさまざまなSqlSessionFactoryを取得できるということです。SqlSessionFactoryが同じでない場合、SqlSessionが取得されると、ThreadLocalで取得されないため、毎回新しいSqlSessionオブジェクトになります。

SqlSessionは異なるため、接続接続を取得すると、動的データソースに移動して毎回取得します。

原則はそのような原則です、散歩しましょう。

構成を変更した後、Serviceメソッドにトランザクションアノテーションを追加します。この時点でデータを正常に更新できます。

@Transactional
@Override
public void createOrder(Order order) {
    storageMapper.decreaseStorage(order);
    orderMapper.createOrder(order);
}

データソースを切り替えることができるのは最初のステップにすぎず、必要な保証によってトランザクション操作を保証できます。上記のコードで在庫の控除は完了しているが、注文の作成が失敗した場合、在庫はロールバックされません。それらは異なるデータソースに属しているため、まったく同じ接続ではありません。

4、XAプロトコル分散トランザクション

上記の問題を解決するために、XAプロトコルしか検討できません。

XAプロトコルとは何かについて、著者はあまり説明しません。MySQLInnoDBストレージエンジンがXAトランザクションをサポートしていることを知っておく必要があるだけです。

次に、XAプロトコルの実現は、Java Transaction Manager、略してJavaではJTAと呼ばれます。

JTAを実装する方法は?最初に、Atomikosフレームワークを使用してその依存関係を導入します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
    <version>2.2.7.RELEASE</version>
</dependency>

次に、DataSourceオブジェクトをAtomikosDataSourceBeanに変更するだけです。

public DataSource getDataSource(Environment env, String prefix, String dataSourceName){
    Properties prop = build(env,prefix);
    AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
    ds.setXaDataSourceClassName(MysqlXADataSource.class.getName());
    ds.setUniqueResourceName(dataSourceName);
    ds.setXaProperties(prop);
    return ds;
}

この構成の後、接続接続を取得すると、実際にはMysqlXAConnectionオブジェクトを取得します。送信またはロールバックするとき、MySQLのXAプロトコルが使用されます。

public void commit(Xid xid, boolean onePhase) throws XAException {
    //封装 XA COMMIT 请求
    StringBuilder commandBuf = new StringBuilder(300);
    commandBuf.append("XA COMMIT ");
    appendXid(commandBuf, xid);
    try {
        //交给MySQL执行XA事务操作
        dispatchCommand(commandBuf.toString());
    } finally {
        this.underlyingConnection.setInGlobalTx(false);
    }
}

Atomikosを導入し、DataSourceを変更することで、複数のデータソースの場合、業務にエラーが発生した場合でも、複数のデータベースを正常にロールバックできます。

別の質問ですが、XAプロトコルを使用する必要がありますか?

XAプロトコルは単純に見えますが、いくつかの欠点もあります。といった:

  • パフォーマンスの問題。トランザクションコミットフェーズ中、すべての参加者が同期ブロッキング状態になり、システムリソースを占有し、パフォーマンスのボトルネックを簡単に引き起こし、同時実行性の高いシナリオに対応できなくなります。
  • コーディネーターに単一障害点がある場合、コーディネーターに障害が発生すると、参加者は常にロックされます。
  • マスタースレーブレプリケーションは、一貫性のないトランザクションステータスを生成する可能性があります。

XAプロトコルのいくつかの制限は、MySQLの公式ドキュメントにも記載されています。

https://dev.mysql.com/doc/refman/8.0/en/xa-restrictions.html

また、実際のプロジェクトでは実際に使用していません。このように分散トランザクションの問題を解決するために、この例では実現可能性スキームについてのみ説明します。

総括する

この記事では、SpringBoot + Mybatisの複数のデータソースシナリオを紹介することにより、次の問題を分析します。

  • 複数のデータソースの構成と実現。
  • Springトランザクションモード、複数のデータソースの障害の理由と解決策。
  • 複数のデータソース、XAプロトコルに基づく分散トランザクションの実装。

著者:静かな場所
出典:ナゲッツ

おすすめ

転載: blog.csdn.net/m0_46757769/article/details/112972459