[Aplicación SpringBoot] SpringBoot integra atomikos para realizar la configuración de múltiples fuentes de datos y la gestión de transacciones distribuidas

Concepto de transacción distribuida

Antes de hablar de transacciones distribuidas, distinguimos dos conceptos: transacciones locales y transacciones distribuidas ;

Las transacciones locales resuelven el problema de coherencia de las operaciones de datos en una sola fuente de datos , mientras que las transacciones distribuidas resuelven el problema de coherencia de las operaciones de datos en múltiples fuentes de datos .

La definición oficial de transacciones distribuidas de Baidu significa que los participantes de las transacciones, los servidores de soporte de transacciones, los servidores de recursos y los administradores de transacciones están ubicados en diferentes nodos de diferentes sistemas distribuidos.

Es decir, en el proceso de operar una lógica de negocios, están involucradas dos fuentes de datos (A, B), y en muchos casos, las dos fuentes de datos de A y B pertenecen a dos entornos físicos diferentes. Cuando ocurre una situación anormal durante la operación de la fuente de datos A, la operación en la fuente de datos B debe revertirse y la operación en la fuente de datos A también debe revertirse.

El uso general de transacciones en el proceso de desarrollo de Java Springnos proporciona un método de transacción declarativo conveniente @transactional. Sin embargo, la transacción predeterminada de Spring solo admite una única fuente de datos . De hecho, un sistema a menudo necesita escribir varias fuentes de datos. En este momento, debemos considerar cómo implementar el soporte para transacciones distribuidas a través de Spring.
SpringBoot proporciona y recomienda oficialmente Atomikos y Bitronix, dos componentes de transacciones distribuidas que no requieren soporte de servidor

La solución para transacciones distribuidas en el campo de JAVA es JTA (Java Transaction API);

Descripción general de XA y JTA

XA es un protocolo para transacciones distribuidas propuesto por la organización X/Open (o denominada arquitectura distribuida). Define principalmente dos partes del administrador, el administrador de transacciones globales y el administrador de recursos. En el concepto de diseño de XA, se incorporan diferentes recursos en un administrador de transacciones para una administración unificada, como recursos de base de datos, recursos de middleware de mensajes, etc., para enviar o cancelar transacciones de todos los recursos. Actualmente, las bases de datos convencionales y el middleware de mensajes admiten protocolo XA.

JTA se llama Java Transaction API, que es la implementación JAVA del protocolo XA. Actualmente en JAVA, la definición de JTA se divide principalmente en dos partes

  • Interfaz del administrador de transacciones -----javax.transaction.TransactionManager
  • Interfaz del administrador de recursos -----javax.transaction.xa.XAResource

En aplicaciones generales, las interfaces JTA se utilizan para implementar transacciones y se requiere un contenedor JTA externo para almacenar estas transacciones, como Tomcat. De lo que vamos a hablar hoy es que Atomikoses un marco que implementa JTA de forma independiente y puede ejecutar transacciones JTA en nuestro servidor de aplicaciones.

SpringBoot integra atomikos

inserte la descripción de la imagen aquí

estructura de la base de datos

inserte la descripción de la imagen aquí

CREATE TABLE `tb_order` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品名称',
  `price` bigint DEFAULT NULL COMMENT '商品价格',
  `num` int DEFAULT '0' COMMENT '商品数量',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=137 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT;

Se agregó el nombre de usuario como un índice único, lo cual es conveniente para pruebas posteriores de reversión de transacciones anormales de múltiples bases de datos de inserción de datos.

CREATE TABLE `tb_user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '收件人',
  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_uername` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=COMPACT;

pompón

pila de tecnología número de versión
bota de resorte 2.3.2.LIBERACIÓN
druida 1.1.10
controlador mysql 8.0.33
mybatis-plus 3.1.1
<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.3.2.RELEASE</version>
</parent>

<dependencies>
    <!-- druid-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.10</version>
    </dependency>
    <!-- druid-->
    <!-- mysql-connector-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
       <version>8.0.33</version>
    </dependency>
    <!-- mysql-connector-->
    <!-- mybatis-plus-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.1.1</version>
    </dependency>
    <!-- mybatis-plus-->

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

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.25</version>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>32.0.0-jre</version>
    </dependency>
</dependencies>


<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

Herramientas generales

R

/**
 * @ClassName: R
 * @Description: 统一返回实体
 */
@Getter
@Setter
@SuppressWarnings({
    
    "AlibabaClassNamingShouldBeCamel"})
@Accessors(chain = true)
public class R<T> {
    
    
    public static final String DEF_ERROR_MESSAGE = "系统繁忙,请稍候再试";
    public static final String HYSTRIX_ERROR_MESSAGE = "请求超时,请稍候再试";
    public static final int SUCCESS_CODE = 0;
    public static final int FAIL_CODE = -1;
    public static final int TIMEOUT_CODE = -2;
    /**
     * 统一参数验证异常
     */
    public static final int VALID_EX_CODE = -9;
    public static final int OPERATION_EX_CODE = -10;
    /**
     * 调用是否成功标识,0:成功,-1:系统繁忙,此时请开发者稍候再试 详情见[ExceptionCode]
     */
    private int code;

    /**
     * 调用结果
     */
    private T data;

    /**
     * 结果消息,如果调用成功,消息通常为空T
     */
    private String msg = "ok";


    private String path;
    /**
     * 附加数据
     */
    private Map<String, Object> extra;

    /**
     * 响应时间
     */
    private long timestamp = System.currentTimeMillis();

    private R() {
    
    
        super();
    }

    public R(int code, T data, String msg) {
    
    
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

    public static <E> R<E> result(int code, E data, String msg) {
    
    
        return new R<>(code, data, msg);
    }

    /**
     * 请求成功消息
     *
     * @param data 结果
     * @return RPC调用结果
     */
    public static <E> R<E> success(E data) {
    
    
        return new R<>(SUCCESS_CODE, data, "ok");
    }

    public static R<Boolean> success() {
    
    
        return new R<>(SUCCESS_CODE, true, "ok");
    }

    /**
     * 请求成功方法 ,data返回值,msg提示信息
     *
     * @param data 结果
     * @param msg  消息
     * @return RPC调用结果
     */
    public static <E> R<E> success(E data, String msg) {
    
    
        return new R<>(SUCCESS_CODE, data, msg);
    }

    /**
     * 请求失败消息
     *
     * @param msg
     * @return
     */
    public static <E> R<E> fail(int code, String msg) {
    
    
        return new R<>(code, null, (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg);
    }

    public static <E> R<E> fail(String msg) {
    
    
        return fail(OPERATION_EX_CODE, msg);
    }

    public static <E> R<E> fail(String msg, Object... args) {
    
    
        String message = (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg;
        return new R<>(OPERATION_EX_CODE, null, String.format(message, args));
    }

    public static <E> R<E> fail(BaseExceptionCode exceptionCode) {
    
    
        return validFail(exceptionCode);
    }

    public static <E> R<E> fail(BizException exception) {
    
    
        if (exception == null) {
    
    
            return fail(DEF_ERROR_MESSAGE);
        }
        return new R<>(exception.getCode(), null, exception.getMessage());
    }

    /**
     * 请求失败消息,根据异常类型,获取不同的提供消息
     *
     * @param throwable 异常
     * @return RPC调用结果
     */
    public static <E> R<E> fail(Throwable throwable) {
    
    
        return fail(FAIL_CODE, throwable != null ? throwable.getMessage() : DEF_ERROR_MESSAGE);
    }

    public static <E> R<E> validFail(String msg) {
    
    
        return new R<>(VALID_EX_CODE, null, (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg);
    }

    public static <E> R<E> validFail(String msg, Object... args) {
    
    
        String message = (msg == null || msg.isEmpty()) ? DEF_ERROR_MESSAGE : msg;
        return new R<>(VALID_EX_CODE, null, String.format(message, args));
    }

    public static <E> R<E> validFail(BaseExceptionCode exceptionCode) {
    
    
        return new R<>(exceptionCode.getCode(), null,
                (exceptionCode.getMsg() == null || exceptionCode.getMsg().isEmpty()) ? DEF_ERROR_MESSAGE : exceptionCode.getMsg());
    }

    public static <E> R<E> timeout() {
    
    
        return fail(TIMEOUT_CODE, HYSTRIX_ERROR_MESSAGE);
    }


    public R<T> put(String key, Object value) {
    
    
        if (this.extra == null) {
    
    
            this.extra = Maps.newHashMap();
        }
        this.extra.put(key, value);
        return this;
    }

    /**
     * 逻辑处理是否成功
     *
     * @return 是否成功
     */
    public Boolean getIsSuccess() {
    
    
        return this.code == SUCCESS_CODE || this.code == 200;
    }

    /**
     * 逻辑处理是否失败
     *
     * @return
     */
    public Boolean getIsError() {
    
    
        return !getIsSuccess();
    }

    @Override
    public String toString() {
    
    
        return JSONObject.toJSONString(this);
    }
}

BaseController

/**
 * @ClassName: BaseController
 * @Description: controller 抽象基类
 */
public abstract class BaseController {
    
    
    /**
     * 成功返回
     *
     * @param data
     * @return
     */
    public <T> R<T> success(T data) {
    
    
        return R.success(data);
    }

    public R<Boolean> success() {
    
    
        return R.success();
    }

    /**
     * 失败返回
     *
     * @param msg
     * @return
     */
    public <T> R<T> fail(String msg) {
    
    
        return R.fail(msg);
    }

    public <T> R<T> fail(String msg, Object... args) {
    
    
        return R.fail(msg, args);
    }

    /**
     * 失败返回
     *
     * @param code
     * @param msg
     * @return
     */
    public <T> R<T> fail(int code, String msg) {
    
    
        return R.fail(code, msg);
    }

    public <T> R<T> fail(BaseExceptionCode exceptionCode) {
    
    
        return R.fail(exceptionCode);
    }

    public <T> R<T> fail(BizException exception) {
    
    
        return R.fail(exception);
    }

    public <T> R<T> fail(Throwable throwable) {
    
    
        return R.fail(throwable);
    }

    public <T> R<T> validFail(String msg) {
    
    
        return R.validFail(msg);
    }

    public <T> R<T> validFail(String msg, Object... args) {
    
    
        return R.validFail(msg, args);
    }

    public <T> R<T> validFail(BaseExceptionCode exceptionCode) {
    
    
        return R.validFail(exceptionCode);
    }
}

BaseExceptionCode


/**
 * @ClassName: BaseExceptionCode
 * @Description: 公共异常编码类
 */
public interface BaseExceptionCode {
    
    
    /**
     * 异常编码
     *
     * @return
     */
    int getCode();

    /**
     * 异常消息
     * @return
     */
    String getMsg();
}

Código de excepción

/**
 * 全局错误码 10000-15000
 * <p>
 * 预警异常编码    范围: 30000~34999
 * 标准服务异常编码 范围:35000~39999
 * 邮件服务异常编码 范围:40000~44999
 * 短信服务异常编码 范围:45000~49999
 * 权限服务异常编码 范围:50000-59999
 * 文件服务异常编码 范围:60000~64999
 * 日志服务异常编码 范围:65000~69999
 * 消息服务异常编码 范围:70000~74999
 * 开发者平台异常编码 范围:75000~79999
 * 搜索服务异常编码 范围:80000-84999
 * 共享交换异常编码 范围:85000-89999
 * 移动终端平台 异常码 范围:90000-94999
 * <p>
 * 安全保障平台    范围:        95000-99999
 * 软硬件平台 异常编码 范围:    100000-104999
 * 运维服务平台 异常编码 范围:  105000-109999
 * 统一监管平台异常 编码 范围:  110000-114999
 * 认证方面的异常编码  范围:115000-115999
 *
 */
public enum ExceptionCode implements BaseExceptionCode {
    
    

    //系统相关 start
    SUCCESS(0, "成功"),
    SYSTEM_BUSY(-1, "系统繁忙~请稍后再试~"),
    SYSTEM_TIMEOUT(-2, "系统维护中~请稍后再试~"),
    PARAM_EX(-3, "参数类型解析异常"),
    SQL_EX(-4, "运行SQL出现异常"),
    NULL_POINT_EX(-5, "空指针异常"),
    ILLEGALA_ARGUMENT_EX(-6, "无效参数异常"),
    MEDIA_TYPE_EX(-7, "请求类型异常"),
    LOAD_RESOURCES_ERROR(-8, "加载资源出错"),
    BASE_VALID_PARAM(-9, "统一验证参数异常"),
    OPERATION_EX(-10, "操作异常"),


    OK(200, "OK"),
    BAD_REQUEST(400, "错误的请求"),
    /**
     * {@code 401 Unauthorized}.
     *
     * @see <a href="http://tools.ietf.org/html/rfc7235#section-3.1">HTTP/1.1: Authentication, section 3.1</a>
     */
    UNAUTHORIZED(401, "未经授权"),
    /**
     * {@code 404 Not Found}.
     *
     * @see <a href="http://tools.ietf.org/html/rfc7231#section-6.5.4">HTTP/1.1: Semantics and Content, section 6.5.4</a>
     */
    NOT_FOUND(404, "没有找到资源"),
    METHOD_NOT_ALLOWED(405, "不支持当前请求类型"),

    TOO_MANY_REQUESTS(429, "请求超过次数限制"),
    INTERNAL_SERVER_ERROR(500, "内部服务错误"),
    BAD_GATEWAY(502, "网关错误"),
    GATEWAY_TIMEOUT(504, "网关超时"),
    //系统相关 end

    REQUIRED_FILE_PARAM_EX(1001, "请求中必须至少包含一个有效文件"),
    //jwt token 相关 start

    JWT_TOKEN_EXPIRED(40001, "会话超时,请重新登录"),
    JWT_SIGNATURE(40002, "不合法的token,请认真比对 token 的签名"),
    JWT_ILLEGAL_ARGUMENT(40003, "缺少token参数"),
    JWT_GEN_TOKEN_FAIL(40004, "生成token失败"),
    JWT_PARSER_TOKEN_FAIL(40005, "解析token失败"),
    JWT_USER_INVALID(40006, "用户名或密码错误"),
    JWT_USER_ENABLED(40007, "用户已经被禁用!"),
    //jwt token 相关 end

    ;

    private int code;
    private String msg;

    ExceptionCode(int code, String msg) {
    
    
        this.code = code;
        this.msg = msg;
    }

    @Override
    public int getCode() {
    
    
        return code;
    }

    @Override
    public String getMsg() {
    
    
        return msg;
    }


    public ExceptionCode build(String msg, Object... param) {
    
    
        this.msg = String.format(msg, param);
        return this;
    }

    public ExceptionCode param(Object... param) {
    
    
        msg = String.format(msg, param);
        return this;
    }
}


BaseException

/**
 * @ClassName: BaseException
 * @Description: 异常接口类
 */
public interface BaseException {
    
    

    /**
     * 统一参数验证异常码
     */
    int BASE_VALID_PARAM = -9;

    /**
     * 返回异常信息
     *
     * @return
     */
    String getMessage();

    /**
     * 返回异常编码
     *
     * @return
     */
    int getCode();
}

BaseUncheckedException

/**
 * @ClassName: BaseUncheckedException
 * @Description: 非运行期异常基类,所有自定义非运行时异常继承该类
 */
public class BaseUncheckedException extends RuntimeException implements BaseException {
    
    

    private static final long serialVersionUID = -778887391066124051L;

    /**
     * 异常信息
     */
    protected String message;

    /**
     * 具体异常码
     */
    protected int code;

    public BaseUncheckedException(int code, String message) {
    
    
        super(message);
        this.code = code;
        this.message = message;
    }

    public BaseUncheckedException(int code, String format, Object... args) {
    
    
        super(String.format(format, args));
        this.code = code;
        this.message = String.format(format, args);
    }


    @Override
    public String getMessage() {
    
    
        return message;
    }
    @Override
    public int getCode() {
    
    
        return code;
    }
}

BizException

/**
 * @ClassName: BizException
 * @Description: 业务异常 用于在处理业务逻辑时,进行抛出的异常。
 */
public class BizException extends BaseUncheckedException {
    
    

    private static final long serialVersionUID = -3843907364558373817L;

    public BizException(String message) {
    
    
        super(-1, message);
    }

    public BizException(int code, String message) {
    
    
        super(code, message);
    }

    public BizException(int code, String message, Object... args) {
    
    
        super(code, message, args);
    }

    /**
     * 实例化异常
     *
     * @param code    自定义异常编码
     * @param message 自定义异常消息
     * @param args    已定义异常参数
     * @return
     */
    public static BizException wrap(int code, String message, Object... args) {
    
    
        return new BizException(code, message, args);
    }

    public static BizException wrap(String message, Object... args) {
    
    
        return new BizException(-1, message, args);
    }

    public static BizException validFail(String message, Object... args) {
    
    
        return new BizException(-9, message, args);
    }

    public static BizException wrap(BaseExceptionCode ex) {
    
    
        return new BizException(ex.getCode(), ex.getMsg());
    }

    @Override
    public String toString() {
    
    
        return "BizException [message=" + message + ", code=" + code + "]";
    }
}

aplicación.yml

spring:
  datasource:
    druid:
      order:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/cloud_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
        user: root
        password: root
      user:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/cloud_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
        user: root
        password: root



mybatis-plus:
  #mybatis日志
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  level:
    cn.zysheep.dao: debug

Clase de configuración de fuente de datos

OrderXADataSourceConfig

/**
 * @ClassName: OrderXADataSourceConfig
 * @Description: mybatis配置类 Order
 */
@Configuration
@MapperScan(basePackages = OrderXADataSourceConfig.BASE_PACKAGES, sqlSessionTemplateRef = "orderSqlSessionTemplate")
public class OrderXADataSourceConfig {
    
    
    /**
     * 扫描mapper接口包
     */
    static final String BASE_PACKAGES = "cn.zysheep.dao.order";
    /**
     * 扫描的mapper配置文件路径
     */
    private static final String MAPPER_LOCATION = "classpath:/mapper/order/*Mapper.xml";

    /**
     * 将这个对象放入spring容器中(交给Spring管理)
     * @ConfigurationProperties 自动配置属性
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid.order")
    public XADataSource getDataSourceOrder(){
    
    
        // 创建XA连接池
        return new MysqlXADataSource();
    }

    /**
     * 创建Atomikos数据源
     * 注解@DependsOn("druidXADataSourcePre"),在名为druidXADataSourcePre的bean实例化后加载当前bean
     * @param xaDataSource
     * @return
     */
    @Bean
    @DependsOn("getDataSourceOrder")
    @Primary
    public DataSource dataSourceOrder(@Qualifier("getDataSourceOrder") XADataSource xaDataSource){
    
    
        //这里的AtomikosDataSourceBean使用的是spring提供的
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        // 必须为数据源指定唯一标识
        atomikosDataSourceBean.setUniqueResourceName("dataSourceOrder");
        atomikosDataSourceBean.setPoolSize(5);
        atomikosDataSourceBean.setTestQuery("select 1");
        atomikosDataSourceBean.setBorrowConnectionTimeout(3);
        atomikosDataSourceBean.setXaDataSource(xaDataSource);

        return atomikosDataSourceBean;
    }

    /**
     * 创建 SqlSessionFactory
     * @return
     * @throws Exception
     */
    @Bean
    @Primary
    public SqlSessionFactory orderSqlSessionFactory(@Qualifier("dataSourceOrder") DataSource dataSource) throws Exception{
    
    
        // 用来创建 SqlSessionFactory 等同于下面配置
//        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
//            <property name="dataSource" ref="dataSource" />
//            <property name="mapperLocations" value="classpath:mybatis-mapper/*.xml"/>
//        </bean>

        // 在配置sqlSession工厂类的时候,创建的是MybatisSqlSessionFactoryBean,是为了能够正常使用Mybatis-Plus组件的基本功能,比如通用的crud语句绑定
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 设置mybatis的xml所在位置(扫描mybatis的相关xml文件,装配到容器中)
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
        //手动设置session工厂时,需要手动添加分页插件
        Interceptor[] plugins = new Interceptor[1];
        plugins[0] = new PaginationInterceptor();
        sqlSessionFactoryBean.setPlugins(plugins);

        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 通过 SqlSessionFactory 来创建 SqlSessionTemplate
     * @param sqlSessionFactory
     * @return
     */
    @Bean
    @Primary
    public SqlSessionTemplate orderSqlSessionTemplate(@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
    
    
        // SqlSessionTemplate是线程安全的,可以被多个DAO所共享使用
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

UserXADataSourceConfig

/**
 * @ClassName: UserXADataSourceConfig
 * @Description: mybatis配置类 User
 */
@Configuration
@MapperScan(basePackages = UserXADataSourceConfig.BASE_PACKAGES, sqlSessionTemplateRef = "userSqlSessionTemplate")
public class UserXADataSourceConfig {
    
    
    /**
     * 扫描mapper接口包
     */
    static final String BASE_PACKAGES = "cn.zysheep.dao.user";
    /**
     * 扫描的mapper配置文件路径
     */
    private static final String MAPPER_LOCATION = "classpath:/mapper/user/*Mapper.xml";


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

    @Bean
    @DependsOn("getDataSourceUser")
    public DataSource dataSourceUser(@Qualifier("getDataSourceUser") XADataSource xaDataSource){
    
    
        //这里的AtomikosDataSourceBean使用的是spring提供的
        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setUniqueResourceName("dataSourceUser");
        atomikosDataSourceBean.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        atomikosDataSourceBean.setTestQuery("select 1");
        atomikosDataSourceBean.setBorrowConnectionTimeout(3);
        atomikosDataSourceBean.setXaDataSource(xaDataSource);
        return atomikosDataSourceBean;
    }

    /**
     * 创建 SqlSessionFactory
     * @return
     * @throws Exception
     */
    @Bean
    @Primary
    public SqlSessionFactory userSqlSessionFactory(@Qualifier("dataSourceUser") DataSource dataSource) throws Exception{
    
    
        // 用来创建 SqlSessionFactory 等同于下面配置
//        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
//            <property name="dataSource" ref="dataSource" />
//            <property name="mapperLocations" value="classpath:mybatis-mapper/*.xml"/>
//        </bean>

        // 在配置sqlSession工厂类的时候,创建的是MybatisSqlSessionFactoryBean,是为了能够正常使用Mybatis-Plus组件的基本功能,比如通用的crud语句绑定
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 设置mybatis的xml所在位置(扫描mybatis的相关xml文件,装配到容器中)
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
        //手动设置session工厂时,需要手动添加分页插件
        Interceptor[] plugins = new Interceptor[1];
        plugins[0] = new PaginationInterceptor();
        sqlSessionFactoryBean.setPlugins(plugins);

        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 通过 SqlSessionFactory 来创建 SqlSessionTemplate
     * @param sqlSessionFactory
     * @return
     */
    @Bean
    @Primary
    public SqlSessionTemplate userSqlSessionTemplate(@Qualifier("userSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
    
    
        // SqlSessionTemplate是线程安全的,可以被多个DAO所共享使用
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

1. Cada fuente de datos corresponde a una clase de configuración
2. @MapperScanLas anotaciones de cada clase de configuración son diferentes y cada una corresponde a su propia carpeta de interfaz de mapeador (es por eso que las interfaces de mapeador de diferentes fuentes de datos se escriben en carpetas diferentes) 3 ,
Cuando Al configurar la clase de fábrica sqlSession, MybatisSqlSessionFactoryBean se crea para permitir el uso normal de las funciones básicas de los componentes de Mybatis-Plus, como el enlace general de sentencias crud.
4. Al configurar la clase de fábrica, debe especificar las mapper.xmlrutas de almacenamiento respectivas (es por eso que el mapper.xml de diferentes fuentes de datos debe escribirse en carpetas diferentes)
5. Al configurar la clase de fábrica, debe agregar manualmente la paginación enchufe entrar. Debido a que nosotros desactivamos la configuración automática relacionada con la fuente de datos, el método de creación de la clase PaginationInterceptor tradicional no es fácil de usar.

clase de entidad

Orden

@Builder
@Data
@TableName("tb_order")
public class Order {
    
    
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("user_id")
    private Long userId;
    @TableField("name")
    private String name;
    @TableField("price")
    private Long price;
    @TableField("num")
    private Integer num;
}

Usuario

@Builder
@Data
@TableName("tb_user")
public class User {
    
    
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("username")
    private String username;
    @TableField("address")
    private String address;
}

Mapeador

Asignador de pedidos

inserte la descripción de la imagen aquí

public interface OrderMapper extends BaseMapper<Order> {
    
    
}

El archivo de configuración OrderMapper.xml, la ruta de la clase Mapper y la ruta de configuración deben ser coherentes con la ruta de la clase de configuración de la fuente de datos; de lo contrario, se informará un error.
inserte la descripción de la imagen aquí

<?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="cn.zysheep.dao.order.OrderMapper">

</mapper>

Asignador de usuarios

public interface UserMapper extends BaseMapper<User> {
    
    
}

Archivo de configuración UserMapper.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="cn.zysheep.dao.user.UserMapper">

</mapper>

Servicio

Servicio de pedidos

public interface OrderService extends IService<Order> {
    
    
    /**
     * 保存订单
     */
    void saveOrder() throws Exception;
}
@Service
@AllArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    
    

    private final OrderMapper orderMapper;

    private final UserMapper userMapper;

    /**
     * 实现多数据库操作
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveOrder() throws Exception {
    
    
        // order数据源
        Order order = Order.builder()
                .userId(1000L)
                .name("Apple 苹果 iPhone 12 ")
                .price(699900L)
                .num(1).build();
        orderMapper.insert(order);

        // user数据源
        User user = User.builder()
                .id(1001L)
                .address("长沙")
                .username("封于修").build();

        userMapper.insert(user);

        // throw new Exception("12312");
    }
}

Probar transacciones distribuidas

La transacción distribuida de fuente de datos múltiples atomikos se usa de la misma manera que la transacción declarativa de Spring, y la clase o método se anota con @Transactional.

1. Almacenamiento normal

2. La fuente de datos del pedido se guarda correctamente, la fuente de datos del usuario se guarda correctamente, se lanza una excepción en otra parte del método y la transacción del método se revierte

3. La fuente de datos del pedido se guarda correctamente, la fuente de datos del usuario no se guarda y la transacción del método se revierte

4. La fuente de datos del pedido no se pudo guardar, la fuente de datos del usuario no se guardó y la transacción del método se revirtió

Servicio de usuario

public interface UserService extends IService<User> {
    
    
    /**
     * 保存用户
     * @throws Exception
     */
    void saveUser() throws Exception;
}
@Service
@AllArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    

    private final OrderMapper orderMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveUser() throws Exception {
    
    
        // order数据源  抛出异常,方法事务回滚
        Order order = Order.builder()
                .userId(1000L)
                .name("Apple 苹果 iPhone 12 ")
                .price(699900L)
                .num(1).build();
        orderMapper.insert(order);


        // 2、user数据源 抛出异常,方法事务回滚
        User user = User.builder()
                .id(1001L)
                .address("长沙")
                .username("封于修").build();

        saveBatch(Collections.singletonList(user));

        // 1、主方法抛出异常,方法事务回滚
        // throw new Exception("12312");
    }
}

Probar transacciones distribuidas

La transacción distribuida de fuente de datos múltiples atomikos se usa de la misma manera que la transacción declarativa de Spring, y la clase o método se anota con @Transactional.

Aquí probamos principalmente si la adición por lotes proporcionada por Mybaits-Plus admite transacciones distribuidas de múltiples fuentes de datos atomikos.La prueba es que las transacciones anormales en otras fuentes de datos dentro del método pueden revertirse;

Controlador

Controlador de pedidos

@RestController
@RequestMapping("/order")
@AllArgsConstructor
public class OrderController extends BaseController {
    
    

    private final OrderService orderService;

    @PostMapping("/save")
    public R save() throws Exception {
    
    
        orderService.saveOrder();
        return success();
    }
}

Controlador de usuario

@RestController
@RequestMapping("/user")
@AllArgsConstructor
public class UserController extends BaseController {
    
    
    private final UserService userService;

    @PostMapping("/batchSave")
    public R batchSave() throws Exception {
    
    
        userService.saveUser();
        return success();
    }
}

clase de inicio

@SpringBootApplication
public class AtomikosApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(AtomikosApplication.class, args);
    }
}

Supongo que te gusta

Origin blog.csdn.net/qq_45297578/article/details/131744977
Recomendado
Clasificación