ホームページ  >  記事  >  Java  >  Spring Boot + MyBatis + Atomikos + MySQL (ソースコード付き)

Spring Boot + MyBatis + Atomikos + MySQL (ソースコード付き)

Java后端技术全栈
Java后端技术全栈転載
2023-08-15 16:12:53635ブラウズ

実際のプロジェクトでは、分散トランザクションを避けるように努めます。 ただし、場合によってはサービスを分割する必要があり、分散トランザクションの問題が発生することがあります。

同時に、分散取引では面接で市場についても質問されるので、このケースで練習して、面接で 123 について話すことができます。

ここはビジネス上の問題です。ユーザーがクーポンを受け取ると、ユーザーがクーポンを受け取った回数を差し引く必要があり、その後、ユーザーがクーポンを受け取った記録が作成されます。記録された。

Spring Boot + MyBatis + Atomikos + MySQL (ソースコード付き)#分割前
Spring Boot + MyBatis + Atomikos + MySQL (ソースコード付き)分割後
当初はメッセージを使用できましたここではキュー モードで、非同期を使用して新しいユーザー コレクション レコードを追加します。ただし、ここでの要件は、ユーザーが収集記録を受け取った後すぐに表示できる必要があることであるため、分散トランザクションの問題を実装するために

Amitikos を導入しました。

分散トランザクション

分散トランザクションは、複数のコンピュータまたはデータベースにまたがるトランザクションを指します。ネットワークの遅延、障害、またはこれらのコンピュータまたはデータベース間の不一致が考えられます。分散トランザクションでは、データの正確性と整合性を確保するために、すべての操作の原子性、一貫性、分離性、耐久性を確保する必要があります。

#分散トランザクション プロトコルとは何ですか? 分散トランザクション プロトコルには、2PC (2 フェーズ コミット) と 3PC (3 フェーズ コミット) の 2 つの主なタイプがあります。

2PC は現在最も一般的に使用されている分散トランザクション プロトコルであり、そのプロセスは準備段階と送信段階の 2 つの段階に分かれています。準備フェーズでは、トランザクション コーディネーターがすべての参加者に準備リクエストを発行し、参加者はローカル トランザクションを実行して準備状態にし、準備結果をトランザクション コーディネーターに返します。コミット フェーズでは、すべての参加者が正常に実行された場合、トランザクション コーディネーターはすべての参加者にコミット リクエストを発行し、参加者はローカル トランザクションをコミットします。それ以外の場合、トランザクション コーディネーターはすべての参加者にロールバック リクエストを発行し、参加者はローカル トランザクションをロールバックします。取引のロールです。

3PC は 2PC の改良版で、2PC に基づいて準備と提出の段階が追加されています。送信準備フェーズでは、コーディネーターが参加者に送信できるかどうかを尋ねます。参加者が同意を返した場合は送信フェーズで直接送信され、そうでない場合は送信フェーズでロールバックされます。

#分散トランザクションの一般的なソリューションは何ですか?

分散トランザクション ソリューションの実装ソリューションには次のものが含まれます。

  • メッセージ キューに基づく分散トランザクション ソリューション (RocketMQ のオープン ソース ソリューションなど)
  • 分散トランザクション フレームワーク (Seata、TCC-Transaction、その他のフレームワークなど) に基づく分散トランザクション ソリューション
  • XA プロトコルに基づく分散トランザクション ソリューション (例: JTA など)
  • #信頼性の高いメッセージの結果整合性に基づく分散トランザクション ソリューション (Alibaba の分散トランザクション ミドルウェア GTS など)
  • # #分散トランザクション スキームCAP 原則に基づく (CQRS アーキテクチャのイベント ソーシング モードなど)
JTA とは何ですか?

JTA (Java Transaction API) は、J2EE のプログラミング インターフェイス仕様であり、XA プロトコルの JAVA 実装です。これは主に以下を定義します。

トランザクション マネージャー インターフェイス

javax.transaction.TransactionManager

。これは、トランザクションの開始、送信、引き出し、およびその他の操作を定義します。

XA 仕様を満たすリソース定義インターフェイスjavax.transaction.xa.XAResource

。リソースが JTA トランザクションをサポートする必要がある場合、そのリソースは

XAResourceInterface を作成し、このインターフェイスで定義された 2 フェーズ送信関連のインターフェイスを実装します。 JTA インターフェースを使用してトランザクションを実装するアプリケーションがある場合、アプリケーションの実行中には、JTA を実装するコンテナーが必要になります。通常、これは JBoss、Websphere、その他のアプリケーション サーバーなどの J2EE コンテナーです。 ただし、JTA を実装する独立したフレームワークもいくつかあります。たとえば、Amitikos や bitronix は JTA 実装フレームワークを jar パッケージの形式で提供しています。このようにして、JTA を使用して Tomcat や Jetty などのサーバー上でトランザクションを実装するアプリケーション システムを実行できます。

上記のローカル トランザクションと外部トランザクションの違いで述べたように、JTA トランザクションは外部トランザクションであり、複数のリソースのトランザクション性を実装するために使用できます。各リソースによって実装された XAResource を通じて、2 フェーズの送信を正確に制御します。興味のある学生は、このインターフェースのメソッドを見てみることができます。コミット、ロールバックなどのメソッドに加えて、end()forget()isSameRM もあります。 ()prepare() など。これらのインターフェイスだけから、2 フェーズ トランザクションの実装における JTA の複雑さが想像できます。

XA とは何ですか?

XA は、X/Open 組織によって提案された分散トランザクション アーキテクチャ (またはプロトコル) です。 XA アーキテクチャは主に、(グローバル) トランザクション マネージャーと (ローカル) リソース マネージャー間のインターフェイスを定義します。 XA インターフェイスは、トランザクション マネージャーと 1 つ以上のリソース マネージャー間の通信ブリッジを形成する双方向システム インターフェイスです。つまり、XAに基づくトランザクションでは、システムが複数のデータベースにアクセスしたり、データベースとメッセージミドルウェアなどのリソースの両方にアクセスしたりするなど、複数のリソースに対するトランザクション管理が可能となります。このようにして、送信されたすべてのトランザクションまたはキャンセルされたすべてのトランザクションを複数のデータベースおよびメッセージ ミドルウェアに直接実装できます。 XA仕様はJava仕様ではなく汎用仕様であり、現在、さまざまなデータベースや多くのメッセージミドルウェアがXA仕様をサポートしています。

JTA は XA 仕様を満たす仕様であり、Java 開発に使用されます。したがって、JTA を使用して分散トランザクションを実装すると言うとき、実際には、JTA 仕様を使用して、システム内の複数のデータベース、メッセージ ミドルウェア、およびその他のリソースとのトランザクションを実装することを意味します。

Amitikos とは

Atomikos は非常に人気のあるオープンソース トランザクション マネージャーであり、Spring Boot アプリケーションに組み込むことができます。 Tomcat アプリケーション サーバーは JTA 仕様を実装していません。Tomcat をアプリケーション サーバーとして使用する場合は、サードパーティのトランザクション マネージャー クラスをグローバル トランザクション マネージャーとして使用する必要があります。これを行うのは Amitikos フレームワークであり、トランザクション管理をアプリケーションに統合します。アプリケーションサーバーに依存しません。

Spring Boot は Atomikos を統合します

たくさんの理論について話すのは無駄です。コードを見せてください。

テクノロジー スタック:Spring Boot MyBatis Atomikos MySQL

この記事のコードに従う場合は、mysql のバージョンに注意してください。

最初に 2 つのデータベース (my-db_0 と my-db_1) を構築し、次に各データベースにテーブルを作成します。

データベース my-db_0 内:

CREATE TABLE `t_user_0` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`age` int NOT NULL,
`gender` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;

データベース my-db_1 内:

CREATE TABLE `t_user_1` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`age` int NOT NULL,
`gender` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8;

これは分散トランザクションを説明するためだけのものであり、テーブル。

#プロジェクト全体の構造

Spring Boot + MyBatis + Atomikos + MySQL (ソースコード付き)プロジェクト全体の構造

# #maven 設定
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tian</groupId>
    <artifactId>spring-boot-atomikos</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>

    <name>spring-boot-atomikos</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- mybatis依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mysql依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!--分布式事务-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 要使生成的jar可运行,需要加入此插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
            </resource>
            <resource>
                <!-- 编译xml文件 -->
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>
</project>

プロパティ設定
server.port=9001
spring.application.name=atomikos-demo

spring.datasource.user0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.user0.url=jdbc:mysql://localhost:3306/my-db_0?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
spring.datasource.user0.user=root
spring.datasource.user0.password=123456

spring.datasource.user1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.user1.url=jdbc:mysql://localhost:3306/my-db_1?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
spring.datasource.user1.user=root
spring.datasource.user1.password=123456

mybatis.mapperLocations=classpath:/com/tian/mapper/*/*.xml
mybatis.typeAliasesPackage=com.tian.entity
mybatis.configuration.cache-enabled=true

データ ソース
/**
 * @author tianwc  公众号:java后端技术全栈、面试专栏
 * @version 1.0.0
 * @date 2023年05月11日 19:38
 * 博客地址:<a href="http://woaijava.cc/">博客地址</a>
 * <p>
 * 配置好两个数据源
 */
@Configuration
public class DataSourceConfig {

    // 将这个对象放入spring容器中(交给Spring管理)
    @Bean
    // 读取 application.yml 中的配置参数映射成为一个对象
    @ConfigurationProperties(prefix = "spring.datasource.user0")
    public XADataSource getDataSource0() {
        // 创建XA连接池
        return new MysqlXADataSource();
    }

    /**
     * 创建Atomikos数据源
     * 注解@DependsOn("druidXADataSourcePre"),在名为druidXADataSourcePre的bean实例化后加载当前bean
     */
    @Bean
    @DependsOn("getDataSource0")
    @Primary
    public DataSource dataSourcePre(@Qualifier("getDataSource0") XADataSource xaDataSource) {
        //这里的AtomikosDataSourceBean使用的是spring提供的
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(xaDataSource);
        atomikosDataSourceBean.setMaxPoolSize(20);
        return atomikosDataSourceBean;
    }


    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.user1")
    public XADataSource getDataSource1() {
        // 创建XA连接池
        return new MysqlXADataSource();
    }

    @Bean
    @DependsOn("getDataSource1")
    public DataSource dataSourceSit(@Qualifier("getDataSource1") XADataSource xaDataSource) {
        //这里的AtomikosDataSourceBean使用的是spring提供的
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSource(xaDataSource);
        return atomikosDataSourceBean;
    }
}

MyBatis Scan
@Configuration
@MapperScan(basePackages = {"com.tian.mapper.user0"}, sqlSessionTemplateRef = "preSqlSessionTemplate")
public class MybatisPreConfig {

    @Autowired
    @Qualifier("dataSourcePre")
    private DataSource dataSource;

    /**
     * 创建 SqlSessionFactory
     */
    @Bean
    @Primary
    public SqlSessionFactory preSqlSessionFactory() throws Exception{
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().
                getResources("classpath*:com/tian/mapper/user0/*.xml"));
        return bean.getObject();
    }

    /**
     * 通过 SqlSessionFactory 来创建 SqlSessionTemplate
     */
    @Bean
    @Primary
    public SqlSessionTemplate preSqlSessionTemplate(@Qualifier("preSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
        // SqlSessionTemplate是线程安全的,可以被多个DAO所共享使用
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
もう 1 つは基本的に同じです。つまり、スキャンパスが次のように変更されます。
("classpath*:com/tian/mapper/user1/*.xml")

mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tian.mapper.user0.User0Mapper">

    <!-- -->
    <cache eviction="LRU" flushInterval="10000" size="1024"  />

    <resultMap id="BaseResultMap" type="com.tian.entity.User0">
        <id column="id" jdbcType="BIGINT" property="id" />
        <result column="user_name" jdbcType="VARCHAR" property="userName" />
        <result column="age" jdbcType="INTEGER" property="age" />
        <result column="gender" jdbcType="INTEGER" property="gender" />
    </resultMap>

    <sql id="Base_Column_List">
        id, user_name, age, gender
    </sql>
    <insert id="insert" parameterType="com.tian.entity.User0">
        insert into t_user_0 (id, user_name,age, gender)
        values (#{id,jdbcType=BIGINT}, #{userName,jdbcType=VARCHAR},#{age,jdbcType=INTEGER},#{gender,jdbcType=INTEGER})
    </insert>

</mapper>
もう 1 つも基本的に同じなので、ここに投稿しました。

対応するマッパー インターフェイスも非常にシンプルです。以下にその 1 つを示します:

public interface User0Mapper {

    int insert(User0 record);
}

service

/**
 * @author tianwc  公众号:java后端技术全栈、面试专栏
 * @version 1.0.0
 * @date 2023年05月11日 19:38
 * 博客地址:<a href="http://woaijava.cc/">博客地址</a>
 * <p>
 * 模拟三种场景:正常、制造异常、数据库异常
 */
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private User0Mapper user0Mapper;
    @Resource
    private User1Mapper user1Mapper;
    /**
     * 正常逻辑 同时对两个数据库进行 插入数据
     */
    @Transactional
    @Override
    public int transaction1() throws Exception {
        User1 user1 = new User1();
        user1.setUserName("22222");
        user1.setAge(11);
        user1.setGender(0);
        user1Mapper.add(user1);
        System.out.println("---------------------------");
        // sit(数据源1)
        User0 user0 = new User0();
        user0.setUserName("111111");
        user0.setAge(11);
        user0.setGender(0);
        user0Mapper.insert(user0);
        return 1;
    }
    /**
     * 正常逻辑 同时对两个数据库进行 插入数据
     * 数据插入完后  出现异常
     */
    @Transactional
    @Override
    public int transaction2() throws Exception {
        User1 user1 = new User1();
        user1.setUserName("22222");
        user1.setAge(11);
        user1.setGender(0);
        user1Mapper.add(user1);
        System.out.println("---------------------------");
        // sit(数据源1)
        User0 user0 = new User0();
        user0.setUserName("111111");
        user0.setAge(11);
        user0.setGender(0);
        user0Mapper.insert(user0);
        //认为制造一个异常
        int a=1/0;
        return 1;
    }

    /**
     * 第一个数据插入成功  第二个数据插入失败
     */
    @Transactional
    @Override
    public int transaction3() throws Exception {
        User1 user1 = new User1();
        user1.setUserName("22222");
        user1.setAge(11);
        user1.setGender(0);
        user1Mapper.add(user1);
        System.out.println("---------------------------");
        // sit(数据源1)
        User0 user0 = new User0();
       //故意搞长点,模拟插入失败 让前面的数据回滚 user0.setUserName("111110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001");
        user0.setAge(11);
        user0.setGender(0);
        user0Mapper.insert(user0);
        return 1;
    }
}

コントローラー

##
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    @PostMapping("/test1")
    public CommonResult test1() {
        int i = 0;
        try {
            i = userService.transaction1();
            return CommonResult.success(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return CommonResult.success(i);
    }

    @PostMapping("/test2")
    public CommonResult test2() {
        int i = 0;
        try {
            i = userService.transaction2();
            return CommonResult.success(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return CommonResult.success(i);
    }

    @PostMapping("/test3")
    public CommonResult test3() {
        int i = 0;
        try {
            i = userService.transaction3();
            return CommonResult.success(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return CommonResult.success(i);
    }
}
#プロジェクト起動クラス

##
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

/**
 * @author tianwc  公众号:java后端技术全栈、面试专栏
 * @version 1.0.0
 * @date 2023年05月11日 19:38
 * 博客地址:<a href="http://woaijava.cc/">博客地址</a>
 * <p>
 * 项目启动类
 */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
//@ComponentScan(basePackages = {"com.tian"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

##テスト

プロジェクトを開始し、次の 3 つをそれぞれテストします:

#http://localhost:9001/user/test1 結果: 両方のデータベースに、新しいテーブル データが作成されます。

http://localhost:9001/user/test2

結果: 除数をゼロにすることはできないという例外がスローされ、新しいデータはどちらのデータベースにも追加されません。

http://localhost:9001/user/test3 結果: データ フィールド値が長すぎる例外がスローされ、新しいデータはどちらのデータベースにも追加されません。

# さて、分散トランザクションを実装しました。

以上がSpring Boot + MyBatis + Atomikos + MySQL (ソースコード付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はJava后端技术全栈で複製されています。侵害がある場合は、admin@php.cn までご連絡ください。