Spring Boot 多数据源,整合 Atomikos 实现分布式事务

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 最近的项目需要整合两个数据库,有些业务逻辑也涉及到两个数据库同时插入、更新的操作;所以就涉及到跨数据库的数据一致性问题。于是基于 `Spring Boot` 整合了 `Atomikos` 的一个项目 demo。

前言

由于最近的项目需要整合两个数据库,有些业务逻辑也涉及到两个数据库同时插入、更新的操作;所以就涉及到跨数据库的数据一致性问题。于是基于 Spring Boot 整合了 Atomikos 的一个项目 demo。
项目源码地址:https://github.com/WongMinHo/spring-boot-api-starter

介绍

  • 分布式事务:

分布式事务,可以理解为:由于分布式而引起的事务不一致的问题。随着项目做大,模块拆分,数据库拆分。一次包含增删改操作数据库涉及到了更新两个不同物理节点的数据库,这样的数据库事务只能保证自己处理的部分的事务,但是整个的事务就不能保证一致性。

  • JTA:

JTA(java Transaction API)是 JavaEE 13 个开发规范之一,java 事务API,允许应用程序执行分布式事务处理——在两个或多个网络计算机资源上访问并且更新数据。JDBC 驱动程序的 JTA 支持极大地增强了数据访问能力。事务就是保证数据的有效性,数据的一致性。

  • Atomikos:

Atomikos 是一个为 Java 平台提供增值服务的并且开源类事务管理器,主要用于处理跨数据库事务;在 Spring Boot 的文档也推荐更多人使用 Atomikos

实现案例

场景:两个数据库,分别是minhow_firstminhow_second;包含 mh_user 用户表、 mh_customer 客户表。

项目结构:
directory-structure

pom.xml 依赖:
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.minhow</groupId>
    <artifactId>spring-boot-api-starter</artifactId>
    <version>1.0</version>
    <name>spring-boot-api-starter</name>
    <description>Spring Boot Seed Project</description>

    <properties>
        <java.version>1.8</java.version>
        <mybatis-plus.version>3.2.0</mybatis-plus.version>
        <mybatis-plus-generator.version>3.2.0</mybatis-plus-generator.version>
        <guava.version>27.1-jre</guava.version>
        <common-lang3.version>3.9</common-lang3.version>
        <fastjson.version>1.2.60</fastjson.version>
        <druid.version>1.1.20</druid.version>
        <jjwt.version>0.9.1</jjwt.version>
        <velocity-engine.version>2.0</velocity-engine.version>
        <mysql-connector.version>8.0.11</mysql-connector.version>
        <lombok.version>1.18.10</lombok.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- jta-atomikos 分布式事务管理 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <!-- mysql connector-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector.version}</version>
        </dependency>

        <!-- mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus-generator.version}</version>
        </dependency>
        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>${velocity-engine.version}</version>
        </dependency>

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Alibaba -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!-- jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
        </dependency>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
application.yml 数据源配置:
# 本地环境配置文件
spring:
  datasource:
    druid:
      first:  #数据源1
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.xa.DruidXADataSource
        url: jdbc:mysql://localhost:3306/minhow_first?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&characterEncoding=utf8
        username: root
        password: root
        #初始化时建立物理连接的个数
        initial-size: 5
        #池中最大连接数
        max-active: 20
        #最小空闲连接
        min-idle: 1
        #获取连接时最大等待时间,单位毫秒
        max-wait: 60000
        #有两个含义:
        #1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
        #2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
        time-between-eviction-runs-millis: 60000
        #连接保持空闲而不被驱逐的最小时间,单位是毫秒
        min-evictable-idle-time-millis: 300000
        #使用该SQL语句检查链接是否可用。如果validationQuery=null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
        validationQuery: SELECT 1 FROM DUAL
        #建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
        test-while-idle: true
        #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
        test-on-borrow: false
        #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
        test-on-return: false
        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,slf4j
        # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      second: #数据源2
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.xa.DruidXADataSource
        url: jdbc:mysql://localhost:3306/minhow_second?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&characterEncoding=utf8
        username: root
        password: root
        #初始化时建立物理连接的个数
        initial-size: 5
        #池中最大连接数
        max-active: 20
        #最小空闲连接
        min-idle: 1
        #获取连接时最大等待时间,单位毫秒
        max-wait: 60000
        #有两个含义:
        #1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
        #2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
        time-between-eviction-runs-millis: 60000
        #连接保持空闲而不被驱逐的最小时间,单位是毫秒
        min-evictable-idle-time-millis: 300000
        #使用该SQL语句检查链接是否可用。如果validationQuery=null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
        validationQuery: SELECT 1 FROM DUAL
        #建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
        test-while-idle: true
        #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
        test-on-borrow: false
        #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
        test-on-return: false
        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,slf4j
        # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
创建两个数据库和数据表sql:
#创建第一个数据库和数据表
CREATE DATABASE minhow_first;
-- ----------------------------
-- Table structure for mh_user
-- ----------------------------
USE minhow_first;
DROP TABLE IF EXISTS `mh_user`;
CREATE TABLE `mh_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(191) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '姓名',
  `password` varchar(191) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '密码',
  `customer_num` int(11) DEFAULT '0' COMMENT '客户数',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

-- ----------------------------
-- Records of mh_user
-- ----------------------------
INSERT INTO `mh_user` VALUES (1, 'minhow', NULL, 0);

#创建第二个数据库和数据表
CREATE DATABASE minhow_second;
-- ----------------------------
-- Table structure for mh_customer
-- ----------------------------
USE minhow_second;
DROP TABLE IF EXISTS `mh_customer`;
CREATE TABLE `mh_customer` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
  `name` varchar(191) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '姓名',
  `phone` varchar(11) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '手机号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
第一个数据源FirstDataSourceProperties配置:
package com.minhow.springbootapistarter.config.datasource;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author MinHow
 * @date 2018/3/4 7:13 下午
 */
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.first")
public class FirstDataSourceProperties {
    private String url;

    private String username;

    private String password;

    private String driverClassName;

    private String type;

    private Integer initialSize;

    private Integer minIdle;

    private Integer maxActive;

    private Integer maxWait;

    private Integer timeBetweenEvictionRunsMillis;

    private Integer minEvictableIdleTimeMillis;

    private String validationQuery;

    private Boolean testWhileIdle;

    private String testOnBorrow;

    private String testOnReturn;

    private String poolPreparedStatements;

    private String filters;

    private String connectionProperties;

    private String initConnectionSqls;
}
第二个数据源SecondDataSourceProperties配置:
package com.minhow.springbootapistarter.config.datasource;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author MinHow
 * @date 2018/3/4 7:13 下午
 */
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.second")
public class SecondDataSourceProperties {
    private String url;

    private String username;

    private String password;

    private String driverClassName;

    private String type;

    private Integer initialSize;

    private Integer minIdle;

    private Integer maxActive;

    private Integer maxWait;

    private Integer timeBetweenEvictionRunsMillis;

    private Integer minEvictableIdleTimeMillis;

    private String validationQuery;

    private Boolean testWhileIdle;

    private String testOnBorrow;

    private String testOnReturn;

    private String poolPreparedStatements;

    private String filters;

    private String connectionProperties;

    private String initConnectionSqls;
}
第一个数据源FirstDataSourceConfiguration配置:

注意:如果使用Druid的分布式驱动,暂不支持MySql8.0+

package com.minhow.springbootapistarter.config.datasource;

import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.minhow.springbootapistarter.common.constant.DBConstants;
import com.mysql.cj.jdbc.MysqlXADataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * @author MinHow
 * @date 2018/3/4  7:20 下午
 */
@Configuration
@MapperScan(basePackages = DBConstants.FIRST_MAPPER, sqlSessionFactoryRef = DBConstants.FIRST_SQL_SESSION_FACTORY)
@Slf4j
public class FirstDataSourceConfiguration {
    @Autowired
    private FirstDataSourceProperties firstDataSourceProperties;

    /**
     * 配置第一个数据源
     * @return
     */
    @Primary
    @Bean(DBConstants.FIRST_DATA_SOURCE)
    public DataSource firstDataSource() {
//        使用Druid的分布式驱动,暂时发现不支持MySql8以上的版本
//        DruidXADataSource druidXADataSource = new DruidXADataSource();
//        BeanUtils.copyProperties(firstDataSourceProperties, druidXADataSource);

        //使用mysql的分布式驱动,支持MySql5.*、MySql8.* 以上版本
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(firstDataSourceProperties.getUrl());
        mysqlXaDataSource.setPassword(firstDataSourceProperties.getPassword());
        mysqlXaDataSource.setUser(firstDataSourceProperties.getUsername());

        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName(DBConstants.FIRST_DATA_SOURCE);
        xaDataSource.setPoolSize(firstDataSourceProperties.getInitialSize());
        xaDataSource.setMinPoolSize(firstDataSourceProperties.getMinIdle());
        xaDataSource.setMaxPoolSize(firstDataSourceProperties.getMaxActive());
        xaDataSource.setMaxIdleTime(firstDataSourceProperties.getMinIdle());
        xaDataSource.setMaxLifetime(firstDataSourceProperties.getMinEvictableIdleTimeMillis());
        xaDataSource.setConcurrentConnectionValidation(firstDataSourceProperties.getTestWhileIdle());
        xaDataSource.setTestQuery(firstDataSourceProperties.getValidationQuery());

        return xaDataSource;
    }

    /**
     * 创建第一个SqlSessionFactory
     * @param firstDataSource
     * @return
     * @throws Exception
     */
    @Primary
    @Bean(DBConstants.FIRST_SQL_SESSION_FACTORY)
    public SqlSessionFactory firstSqlSessionFactory(@Qualifier(DBConstants.FIRST_DATA_SOURCE) DataSource firstDataSource)
            throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(firstDataSource);
        //设置mapper位置
        bean.setTypeAliasesPackage(DBConstants.FIRST_MAPPER);
        //设置mapper.xml文件的路径
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(DBConstants.FIRST_MAPPER_XML));

        return bean.getObject();
    }
}
第二个数据源SecondDataSourceConfiguration配置:

注意:如果使用Druid的分布式驱动,暂不支持MySql8.0+

package com.minhow.springbootapistarter.config.datasource;

import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.minhow.springbootapistarter.common.constant.DBConstants;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * @author MinHow
 * @date 2018/3/4  7:20 下午
 */
@Configuration
@MapperScan(basePackages = DBConstants.SECOND_MAPPER, sqlSessionFactoryRef = DBConstants.SECOND_SQL_SESSION_FACTORY)
public class SecondDataSourceConfiguration {
    @Autowired
    private SecondDataSourceProperties secondDataSourceProperties;

    /**
     * 配置第二个数据源
     * @return
     */
    @Bean(DBConstants.SECOND_DATA_SOURCE)
    public DataSource secondDataSource() {
//        使用Druid的分布式驱动,暂时发现不支持mysql8以上的版本
//        DruidXADataSource druidXADataSource = new DruidXADataSource();
//        BeanUtils.copyProperties(secondDataSourceProperties, druidXADataSource);

        //使用mysql的分布式驱动,支持mysql5.*、mysql8.* 以上版本
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(secondDataSourceProperties.getUrl());
        mysqlXaDataSource.setPassword(secondDataSourceProperties.getPassword());
        mysqlXaDataSource.setUser(secondDataSourceProperties.getUsername());

        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName(DBConstants.SECOND_DATA_SOURCE);
        xaDataSource.setPoolSize(secondDataSourceProperties.getInitialSize());
        xaDataSource.setMinPoolSize(secondDataSourceProperties.getMinIdle());
        xaDataSource.setMaxPoolSize(secondDataSourceProperties.getMaxActive());
        xaDataSource.setMaxIdleTime(secondDataSourceProperties.getMinIdle());
        xaDataSource.setMaxLifetime(secondDataSourceProperties.getMinEvictableIdleTimeMillis());
        xaDataSource.setConcurrentConnectionValidation(secondDataSourceProperties.getTestWhileIdle());
        xaDataSource.setTestQuery(secondDataSourceProperties.getValidationQuery());

        return xaDataSource;
    }

    /**
     * 创建第二个SqlSessionFactory
     * @param secondDataSource
     * @return
     * @throws Exception
     */
    @Bean(DBConstants.SECOND_SQL_SESSION_FACTORY)
    public SqlSessionFactory secondSqlSessionFactory(@Qualifier(DBConstants.SECOND_DATA_SOURCE) DataSource secondDataSource)
            throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(secondDataSource);
        //设置mapper位置
        bean.setTypeAliasesPackage(DBConstants.SECOND_MAPPER);
        //设置mapper.xml文件的路径
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(DBConstants.SECOND_MAPPER_XML));

        return bean.getObject();
    }
}
Atomikos配置:
package com.minhow.springbootapistarter.config.datasource;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

/**
 * 事务管理
 * @author jacker
 * @date 2019/8/13 3:41 PM
 */
@Configuration
@EnableTransactionManagement
public class TransactionManagerConfig {
    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager")
    public TransactionManager atomikosTransactionManager() throws Throwable {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);
        return userTransactionManager;
    }

    @Bean(name = "transactionManager")
    @DependsOn({"userTransaction", "atomikosTransactionManager"})
    public PlatformTransactionManager transactionManager() throws Throwable {
        return new JtaTransactionManager(userTransaction(), atomikosTransactionManager());
    }
}

通过 @EnableTransactionManagement 来启用事务管理,该注解会自动查找满足条件的PlatformTransactionManager;更详细的配置方法可以参见 Atomikos Spring Integration
还有 Dao 和 Mapper 的代码就不贴了,详情请看项目源码。
至此为止,配置就完成了,之后只需要在事务控制的地方加上 @Transactional 注解即可。

案例:

业务流程:在 mh_customer 客户表新增记录,mh_user 用户表客户数增加1,代码如下:

package com.minhow.springbootapistarter.service.second.impl;

import com.minhow.springbootapistarter.common.enums.ResultEnum;
import com.minhow.springbootapistarter.common.exception.BusinessException;
import com.minhow.springbootapistarter.common.response.Result;
import com.minhow.springbootapistarter.pojo.dto.StoreCustomerDTO;
import com.minhow.springbootapistarter.pojo.entity.first.User;
import com.minhow.springbootapistarter.pojo.entity.second.Customer;
import com.minhow.springbootapistarter.dao.second.mapper.CustomerMapper;
import com.minhow.springbootapistarter.service.first.UserService;
import com.minhow.springbootapistarter.service.second.CustomerService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author MinHow
 * @since 2019-10-05
 */
@Service
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements CustomerService {
    @Autowired
    private UserService userService;
    /**
     * 新增客户 - 演示多数据源分布式事务
     * @param storeCustomerDTO
     * @return
     */
    @Override
    @Transactional(rollbackFor = BusinessException.class)
    public Result store(StoreCustomerDTO storeCustomerDTO) {
        User user = userService.lambdaQuery()
                .select(User::getId, User::getCustomerNum)
                .eq(User::getId, storeCustomerDTO.getUserId())
                .one();

        if (user == null) {
            return Result.fail(4001, "用户不存在");
        }

        Customer customer = new Customer();
        customer.setName(storeCustomerDTO.getCustomerName())
                .setPhone(storeCustomerDTO.getCustomerPhone())
                .setUserId(storeCustomerDTO.getUserId());
        //添加客户
        boolean customerStatus = this.save(customer);

        //更新用户客户数
        boolean userStatus = userService.lambdaUpdate()
                .set(User::getCustomerNum, user.getCustomerNum() + 1)
                .eq(User::getId, storeCustomerDTO.getUserId())
                .update();
        //不符合条件,两个数据库表数据回滚
        if (! customerStatus || ! userStatus) {
            throw new BusinessException(ResultEnum.BUSINESS_ERROR);
        }

        return Result.ok();
    }
}

通过修改不同条件,测试事务回滚和不回滚的结果,就能测试分布式事务是否得到支持。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
这篇文章是关于如何在SpringBoot应用中整合Redis并处理分布式场景下的缓存问题,包括缓存穿透、缓存雪崩和缓存击穿。文章详细讨论了在分布式情况下如何添加分布式锁来解决缓存击穿问题,提供了加锁和解锁的实现过程,并展示了使用JMeter进行压力测试来验证锁机制有效性的方法。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解分布式情况下如何添加分布式锁 【续篇】
|
2月前
|
消息中间件 Java 对象存储
数据一致性挑战:Spring Cloud与Netflix OSS下的分布式事务管理
数据一致性挑战:Spring Cloud与Netflix OSS下的分布式事务管理
50 2
|
3月前
|
缓存 NoSQL Java
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
这篇文章介绍了如何在SpringBoot项目中整合Redis,并探讨了缓存穿透、缓存雪崩和缓存击穿的问题以及解决方法。文章还提供了解决缓存击穿问题的加锁示例代码,包括存在问题和问题解决后的版本,并指出了本地锁在分布式情况下的局限性,引出了分布式锁的概念。
SpringBoot整合Redis、以及缓存穿透、缓存雪崩、缓存击穿的理解、如何添加锁解决缓存击穿问题?分布式情况下如何添加分布式锁
|
2月前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
210 1
|
3月前
|
Java 微服务 Spring
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
文章介绍了如何利用Spring Cloud Alibaba快速构建大型电商系统的分布式微服务,包括服务限流降级等主要功能的实现,并通过注解和配置简化了Spring Cloud应用的接入和搭建过程。
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
|
3月前
|
Java
SpringBoot 配置多数据源
SpringBoot 配置多数据源
35 0
|
4月前
|
缓存 NoSQL Java
Spring Boot中的分布式缓存实现
Spring Boot中的分布式缓存实现
|
4月前
|
NoSQL Java Redis
如何在Spring Boot中实现分布式锁
如何在Spring Boot中实现分布式锁
|
4月前
|
缓存 NoSQL Java
在Spring Boot中实现分布式缓存策略
在Spring Boot中实现分布式缓存策略
|
4月前
|
NoSQL Java 调度
在Spring Boot中实现分布式任务调度
在Spring Boot中实现分布式任务调度