分佈式事務(3)---強一致性分佈式事務Atomikos實戰 分佈式事務(1)-理論基礎 分佈式事務(2)---強一致性分佈式事務解決方案 分佈式事務(4)---最終一致性方案之TCC

分佈式事務(1)-理論基礎

分佈式事務(2)---強一致性分佈式事務解決方案

分佈式事務(4)---最終一致性方案之TCC

前面介紹強一致性分佈式解決方案,這裏用Atomikos框架寫一個實戰的demo。模擬下單扣減庫存的操作。

使用Atomikos,mybatis-plus框架搭建項目,springboot版本 2.3.2.RELEASE。

1.項目搭建

依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>

庫存:

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
public class Storage {
    @TableId(type= IdType.AUTO)
    private Integer id;
    
    private Integer commodityId;
    
    private Integer quantity;
    
}

訂單:

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.math.BigDecimal;


@Data
@TableName("t_order")
public class Order {

    @TableId(type= IdType.AUTO)
    private Integer id;
    
    private String userId;

    private Integer commodityId;
    
    private Integer quantity;
    
    private BigDecimal price;
    
    private Integer status;
}

初始化sql:需要建兩個數據庫,我這裏建了一個njytest1和njytest2,讓訂單表和存庫表在不同數據庫生成初始化表數據

CREATE TABLE `storage` (
                           `id` int(11) NOT NULL AUTO_INCREMENT,
                           `commodity_id` int(11) NOT NULL,
                           `quantity` int(11) NOT NULL,
                           PRIMARY KEY (`id`),
                           UNIQUE KEY `idx_commodity_id` (`commodity_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
INSERT INTO `storage` (`id`, `commodity_id`, `quantity`) VALUES (1, 1, 10);

CREATE TABLE `t_order` (
                           `id` int(11) NOT NULL AUTO_INCREMENT,
                           `user_id` varchar(255) DEFAULT NULL,
                           `commodity_id` int(11) NOT NULL,
                           `quantity` int(11) DEFAULT 0,
                           `price` decimal (10,2) DEFAULT NULL ,
                           `status` int(11) DEFAULT NULL,
                           PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2.配置

數據庫1配置類,用於接收數據源1的配置:

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

/**
 * Description:
 * Created by nijunyang on 2021/12/2 23:57
 */
@Data
@ConfigurationProperties(prefix = "mysql.datasource1")
@Component
public class DBConfig1 {
    private String url;
    private String username;
    private String password;
    private int minPoolSize;
    private int maxPoolSize;
    private int maxLifetime;
    private int borrowConnectionTimeout;
    private int loginTimeout;
    private int maintenanceInterval;
    private int maxIdleTime;
    private String testQuery;
}

數據庫2的配置類,用於接收數據源2的配置:

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

/**
 * Description:
 * Created by nijunyang on 2021/12/3 0:00
 */
@Data
@ConfigurationProperties(prefix = "mysql.datasource2")
@Component
public class DBConfig2 {
    private String url;
    private String username;
    private String password;
    private int minPoolSize;
    private int maxPoolSize;
    private int maxLifetime;
    private int borrowConnectionTimeout;
    private int loginTimeout;
    private int maintenanceInterval;
    private int maxIdleTime;
    private String testQuery;
}

application.yml配置文件中對應的兩個數據源配置

mysql:
  datasource1:
    url: jdbc:mysql://localhost:3306/njytest1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    minPoolsize: 3
    maxPoolSize: 25
    maxLifetime: 30000
    borrowConnectionTimeout: 30
    loginTimeout: 30
    maintenanceInterval: 60
    maxIdleTime: 60
    testQuery: SELECT 1

  datasource2:
    url: jdbc:mysql://localhost:3306/njytest2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    minPoolsize: 3
    maxPoolSize: 25
    maxLifetime: 30000
    borrowConnectionTimeout: 30
    loginTimeout: 30
    maintenanceInterval: 60
    maxIdleTime: 60
    testQuery: SELECT 1
logging:
  level:
    com.nijunyang.tx.xa.mapper1: debug
    com.nijunyang.tx.xa.mapper2: debug

我們需要將我們的mapper放到兩個不同的包下面,才能給兩個mapper配置不同的數據源。

 

 

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nijunyang.tx.common.entity.Order;
import org.springframework.stereotype.Repository;

/**
 * Description:
 * Created by nijunyang on 2021/12/3 0:09
 */
@Repository
public interface OrderMapper extends BaseMapper<Order> {

}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nijunyang.tx.common.entity.Storage;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

/**
 * Description:
 * Created by nijunyang on 2021/12/3 0:10
 */
@Repository
public interface StorageMapper extends BaseMapper<Storage> {

    @Update("UPDATE storage SET quantity = quantity - #{quantity} WHERE commodity_id = #{commodityId} and quantity >= #{quantity}")
    void reduce(Integer commodityId, Integer quantity);
}

分別配置兩個數據源的mybatis配置:

 

 

MyBatisConfig1 制定使用com.nijunyang.tx.xa.mapper1包, 並且指定其sqlSessionTemplate 爲名爲orderSqlSessionTemplate的bean;
MyBatisConfig2 制定使用com.nijunyang.tx.xa.mapper2包, 並且指定其sqlSessionTemplate 爲名爲storageSqlSessionTemplate的bean;
也就是這兩個配置:
@MapperScan(basePackages = "com.nijunyang.tx.xa.mapper1", sqlSessionTemplateRef = "orderSqlSessionTemplate")
@MapperScan(basePackages = "com.nijunyang.tx.xa.mapper2", sqlSessionTemplateRef = "storageSqlSessionTemplate")

import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
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 javax.sql.DataSource;
import java.sql.SQLException;

/**
 * Description:
 * Created by nijunyang on 2021/12/3 0:11
 */
@Configuration
/**
 * 制定此mapper使用哪個sqlSessionTemplate
 */
@MapperScan(basePackages = "com.nijunyang.tx.xa.mapper1", sqlSessionTemplateRef = "orderSqlSessionTemplate")
public class MyBatisConfig1 {

    //配置XA數據源
    @Primary
    @Bean(name = "orderDataSource")
    public DataSource orderDataSource(DBConfig1 dbConfig1) throws SQLException {
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(dbConfig1.getUrl());
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
        mysqlXaDataSource.setPassword(dbConfig1.getPassword());
        mysqlXaDataSource.setUser(dbConfig1.getUsername());
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);

        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName("orderDataSource");

        xaDataSource.setMinPoolSize(dbConfig1.getMinPoolSize());
        xaDataSource.setMaxPoolSize(dbConfig1.getMaxPoolSize());
        xaDataSource.setMaxLifetime(dbConfig1.getMaxLifetime());
        xaDataSource.setBorrowConnectionTimeout(dbConfig1.getBorrowConnectionTimeout());
        xaDataSource.setLoginTimeout(dbConfig1.getLoginTimeout());
        xaDataSource.setMaintenanceInterval(dbConfig1.getMaintenanceInterval());
        xaDataSource.setMaxIdleTime(dbConfig1.getMaxIdleTime());
        xaDataSource.setTestQuery(dbConfig1.getTestQuery());
        return xaDataSource;
    }

    @Primary
    @Bean(name = "orderSqlSessionFactory")
    public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource) throws Exception {
//        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
//        bean.setDataSource(dataSource);
        // 這裏用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否則 MyBatisPlus 不會生效
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean.getObject();
    }

    @Primary
    @Bean(name = "orderSqlSessionTemplate")
    public SqlSessionTemplate orderSqlSessionTemplate(
            @Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
        return sqlSessionTemplate;
    }
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
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 javax.sql.DataSource;
import java.sql.SQLException;

/**
 * Description:
 * Created by nijunyang on 2021/12/3 0:16
 */
@Configuration
/**
 * 制定此mapper使用哪個sqlSessionTemplate
 */
@MapperScan(basePackages = "com.nijunyang.tx.xa.mapper2", sqlSessionTemplateRef = "storageSqlSessionTemplate")
public class MyBatisConfig2 {

    //配置XA數據源
    @Bean(name = "storageDataSource")
    public DataSource storageDataSource(DBConfig2 dbConfig2) throws SQLException {
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(dbConfig2.getUrl());
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
        mysqlXaDataSource.setPassword(dbConfig2.getPassword());
        mysqlXaDataSource.setUser(dbConfig2.getUsername());
        mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);

        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName("storageDataSource");

        xaDataSource.setMinPoolSize(dbConfig2.getMinPoolSize());
        xaDataSource.setMaxPoolSize(dbConfig2.getMaxPoolSize());
        xaDataSource.setMaxLifetime(dbConfig2.getMaxLifetime());
        xaDataSource.setBorrowConnectionTimeout(dbConfig2.getBorrowConnectionTimeout());
        xaDataSource.setLoginTimeout(dbConfig2.getLoginTimeout());
        xaDataSource.setMaintenanceInterval(dbConfig2.getMaintenanceInterval());
        xaDataSource.setMaxIdleTime(dbConfig2.getMaxIdleTime());
        xaDataSource.setTestQuery(dbConfig2.getTestQuery());
        return xaDataSource;
    }

    @Bean(name = "storageSqlSessionFactory")
    public SqlSessionFactory storageSqlSessionFactory(@Qualifier("storageDataSource") DataSource dataSource) throws Exception {
//        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
//        bean.setDataSource(dataSource);
        // 這裏用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否則 MyBatisPlus 不會生效
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean.getObject();
    }

    @Bean(name = "storageSqlSessionTemplate")
    public SqlSessionTemplate storageSqlSessionTemplate(
            @Qualifier("storageSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
        return sqlSessionTemplate;
    }
}

因爲我們要使用自己的數據源,所以啓動類需要剔除數據源的自動配置

 

 3.業務代碼

import com.nijunyang.tx.common.entity.Order;
import com.nijunyang.tx.xa.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * Description:
 * Created by nijunyang on 2021/12/3 0:20
 */
@RestController
@RequestMapping("order")
public class OrderController {

    @Resource
    private OrderService orderService;

    //127.0.0.1:8080/order?userId=1&commodityId=1&quantity=2&price=10
    @GetMapping
    public Object create(Order order) {
        orderService.create(order);
        return 1;
    }

}
import com.nijunyang.tx.common.entity.Order;
import com.nijunyang.tx.xa.mapper1.OrderMapper;
import com.nijunyang.tx.xa.mapper2.StorageMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * Description:
 * Created by nijunyang on 2021/12/3 0:21
 */
@Service
public class OrderService {

    @Resource
    private OrderMapper orderMapper;
    @Resource
    private StorageMapper storageMapper;

    @Transactional(rollbackFor = Exception.class)
    public void create(Order order) {
        orderMapper.insert(order);
        storageMapper.reduce(order.getCommodityId(), order.getQuantity());
//        int a = 1/0;
    }
}

 

至此項目搭建完畢,訪問  127.0.0.1:8080/order?userId=1&commodityId=1&quantity=2&price=10,可以發現兩個數據庫的t_order表和 storage數據正常寫入。

當我在業務層構造一個異常 int a = 1/0時,會發現兩個庫均不會寫入數據。

實際上通過@Transactional註解拿到的是這個事務管理器org.springframework.transaction.jta.JtaTransactionManager#doBegin,最終開啓事務是由com.atomikos.icatch.jta.UserTransactionManager#begin來開啓事務,這個就是Atomikos提供的事務管理器;

發生異常回滾也是com.atomikos.icatch.jta.UserTransactionManager#rollback,最終com.atomikos.icatch.imp.TransactionStateHandler#rollback會將所有的事務都回滾。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章