seata distributed transaction solution

Please see gitee for the entire project: https://gitee.com/xwb1056481167/spring-cloud

For configuration and installation of nacos, please go to: https://blog.csdn.net/www1056481167/article/details/113612177 view

seata 官 网 :https://seata.io/zh-cn

The process of distributed transaction is

One ID + three component model

Key concept

  • ID of the
           global transaction
  • Three component concept
    • 1. TC (Transaction Coordinator)-transaction coordinator
      maintains the state of global and branch transactions, and drives global transaction commit or rollback
    • 2. TM (Transaction Manager)-transaction manager
      defines the scope of global transactions: start global transactions, commit or roll back global transactions
    • 3. RM (Resource Manager)-resource manager
      manages the resources of branch transactions, talks with TC to register branch transactions and report the status of branch transactions, and drive branch transactions to commit or roll back

Processing flow chart

Description:
1. TM applies to TC to open a global transaction, and the global transaction is successfully created and a globally unique XID is generated.
2. XID is propagated in the context of microservice propagation link.
3. RM registers branch transaction with TC and incorporates it into XID correspondence Jurisdiction of global affairs
4. TM initiates a global submission or rollback resolution for XID to TC
5. TC schedules all branch transactions under the jurisdiction of XID to complete submission or rollback requests

Seata environment build configuration

1. Download of seata

Download link: https://github.com/seata/seata/releases
Version 0.9: windows:  https://github.com/seata/seata/releases/download/v0.9.0/seata-server-0.9.0.zip

Second, the installation of seata

1. Modify file.conf custom transaction group name + transaction log mode to db + database connection

1. Service's vgroup_mapping. my_test_tx_group attribute

service {
  #vgroup->rgroup
  vgroup_mapping.my_test_tx_group = "fsp_tx_group"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

2. Specify the storage method as the database

store {
  ## store mode: file、db
  mode = "db"
...
## database store
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "123456"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}

3. Create a database seata and create a table

-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `global_table` (
  `xid` varchar(128)  not null,
  `transaction_id` bigint,
  `status` tinyint not null,
  `application_id` varchar(32),
  `transaction_service_group` varchar(32),
  `transaction_name` varchar(128),
  `timeout` int,
  `begin_time` bigint,
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`xid`),
  key `idx_gmt_modified_status` (`gmt_modified`, `status`),
  key `idx_transaction_id` (`transaction_id`)
);

-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
  `branch_id` bigint not null,
  `xid` varchar(128) not null,
  `transaction_id` bigint ,
  `resource_group_id` varchar(32),
  `resource_id` varchar(256) ,
  `lock_key` varchar(128) ,
  `branch_type` varchar(8) ,
  `status` tinyint,
  `client_id` varchar(64),
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`branch_id`),
  key `idx_xid` (`xid`)
);

-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
  `row_key` varchar(128) not null,
  `xid` varchar(96),
  `transaction_id` long ,
  `branch_id` long,
  `resource_id` varchar(256) ,
  `table_name` varchar(32) ,
  `pk` varchar(36) ,
  `gmt_create` datetime ,
  `gmt_modified` datetime,
  primary key(`row_key`)
);

2. Modify the registration information of registry.conf

Modify the registration type to nacos, and specify the registration address of nacos as localhost:8848 (according to your nacos configuration)

For the installation and configuration of nacos, please go to https://blog.csdn.net/www1056481167/article/details/113612177 to view

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
  }
  ...

Whether the startup verification is successfully configured

1. Start nacos
2. Start seata (double-click seata-server.bat to find the registration statement registered in nacos)

Three, prepare the business table structure

DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`  (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户id',
  `total` decimal(10, 0) NULL DEFAULT NULL COMMENT '总额度',
  `used` decimal(10, 0) NULL DEFAULT NULL COMMENT '已用额度',
  `residue` decimal(10, 0) NULL DEFAULT NULL COMMENT '剩余可用额度',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4;

DROP TABLE IF EXISTS `seata_storage`;
CREATE TABLE `t_storage`  (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(11) NULL DEFAULT NULL COMMENT '产品id',
  `total` int(11) NULL DEFAULT NULL COMMENT '库存',
  `used` int(11) NULL DEFAULT NULL COMMENT '已用库存',
  `residue` int(11) NULL DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 ;

DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(11) NULL DEFAULT NULL COMMENT '产品id',
  `count` int(11) NULL DEFAULT NULL COMMENT '数量',
  `money` decimal(11, 0) NULL DEFAULT NULL COMMENT '金额',
  `status` int(1) NULL DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4;

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Final table structure

New order, inventory, balance module

Business requirement: place an order -> reduce inventory -> deduct balance -> change (order) status

Only the core code is listed below, please check all the codes on gitee

New order module seata-order-service2001

1. pom.xml (partially omitted)

<!--  SpringCloud alibaba nacos    -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <!-- 排除自身附带的jar包 -->
        <exclusion>
            <artifactId>seata-all</artifactId>
            <groupId>io.seata</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 引入和服务端相匹配的jar包 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>0.9.0</version>
</dependency>
<!--   openfeign     -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2 、 application.yml

server:
  port: 2001
spring:
  application:
    name: seata-order-service
  cloud:
    nacos:
      discovery: #Nacos注册中心地址
        server-addr: localhost:8848
    alibaba:
      seata:
        tx-service-group: fsp_tx_group
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
#    type: com.alibaba.druid.pool.DruidDataSource
feign:
  hystrix:
    enabled: true

logging:
  level:
    io:
      seata: info
mybatis:
  mapper-locations: classpath:mapper/*.xml

3. Back up seata's configuration file file.conf  registry.conf
4. Provide dao, domain, mapper*.xml, controller .
5. The service  pays special attention to the methods of other microservices called. @FeignClient(value="xx") needs to indicate the method interface to be called. The method provided by the service itself does not need this annotation.

6, config configuration

//1、mybatis扫描的包
@Configuration
@MapperScan("org.xwb.springcloud.dao.*")
public class MyBatisConfig {
}
//2、druid数据源  
@Configuration
public class DataSourceProxyConfig {
    @Value("${mybatis.mapper-locations}")
    private String mapperLocations;
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }
    @Primary
    @Bean("dataSource")
    public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSourceProxy);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        bean.setMapperLocations(resolver.getResources(mapperLocations));
        SqlSessionFactory factory;
        try {
            factory = bean.getObject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return factory;
    }
}

7, the main start class

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataOrderMainApp2001 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}

Pay special attention to  @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) to
exclude your own data source and use your own defined data source

Create a new inventory module seata-storage-service2002

Same as 2001 above, only the differences are explained below
1. application.xml

server:
  port: 2002
spring:
  application:
    name: seata-storage-service
  cloud:
    nacos:
      discovery: #Nacos注册中心地址
        server-addr: localhost:8848
    alibaba:
      seata:
        tx-service-group: fsp_tx_group
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
#    type: com.alibaba.druid.pool.DruidDataSource
feign:
  hystrix:
    enabled: true

logging:
  level:
    io:
      seata: info
mybatis:
  mapper-locations: classpath:mapper/*.xml

Other businesses can directly view the project

New account module seata-account-service2003

Same as 2003, only the differences are listed below

server:
  port: 2003

spring:
  application:
    name: seata-account-service
  cloud:
    alibaba:
      seata:
        tx-service-group: fsp_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
#    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: 123456
feign:
  hystrix:
    enabled: true
logging:
  level:
    io:
      seata: info
mybatis:
  mapper-locations: classpath*:mapper/*.xml

For other business reference code original projects, please go to gitee to view

Test ( normal test, GlobalTransactional )

Normal test (Test without GlobalTransactional unified transaction )

1. Start nacos, seata, 2001, 2002, 2003 

2. Enter http://localhost2001/order/create?userId=1&productId=1&count=1&money=100 in the browser to  check the inventory is correct
3. At this time, the payment of the seata-account-service2003 account needs to be changed ( modified to timeout, (Account deduction failed )

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
    @Resource
    private AccountDao accountDao;
    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("********->account-service中扣减账户余额开始");
        //此处故意超时
        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        accountDao.decrease(userId, money);
        log.info("********->account-service中扣减账户余额结束");
    }
}

Test Results

You will find that the order is successfully created, but the status of the order is incorrect.

The analysis shows that:

The whole thing is divided into 4 parts 1. New order 2. Deduction of inventory 3. Deduction of account 4. Modification of order status

The previous 3 parts were all executed successfully, but the final need to modify the status of the order did fail. Because we set a timeout, we deliberately let the wait timeout process. At this time, because we did not add the control of the global transaction, the halfway failure failed to execute successfully, but in his prior to

If the execution is successful, it cannot be rolled back, which causes the whole thing to be incorrect. The order was placed, the payment was deducted, and the order status was changed but it was wrong. At this time, a global transaction is needed to solve the problem of inconsistency of things.

Use GlobalTransactional

Through the above test, you will find that the transaction processing is not uniform. Next, use seata's global transaction to control

1. Add the annotation @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) name can be customized

@Override
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
public void create(Order order) {
    log.info("--------->开始新建订单");
    //1 新建订单
    orderDao.create(order);
    //2 扣减库存
    log.info("------------->订单微服务开始调用库存,做扣减Count");
    storageService.decrease(order.getProductId(), order.getCount());
    log.info("------------->订单微服务开始调用库存,做扣减end");
    //3 扣减账户
    log.info("------------->订单微服务开始调用账户,做扣减Money");
    accountService.decrease(order.getUserId(), order.getMoney());
    log.info("------------->订单微服务开始调用账户,做扣减end");
    //4 修改订单状态
    log.info("------------->修改订单状态开始");
    orderDao.update(order.getUserId(), 0);
    log.info("------------->修改订单状态结束");
    log.info("------------->下订单结束了");
}

Restart the application, request the order again, you will find that the entire order cannot be added, and all the orders are rolled back

Principle analysis

TM starts distributed transactions (TM registers global transaction records
with TC) according to business scenarios, arranges database, server and other transaction resources (RM reports resource preparation status to TC
) TM ends distributed transactions, and the first stage of the transaction is ready (TM informs TC to submit , Rolling back distributed transactions)
TC reports transaction information and decides whether the distributed transaction is to be submitted or rolled back.
TC notifies all RMs to submit the rollback resources, and the second phase of the transaction ends

Core principle analysis

One-stage loading

In the first stage, Seata will intercept "business SQL"

1. Analyze sql semantics, find the business data to be updated in "business SQL", save it to "before image" before the business data is updated,
2. execute "business SQL" to update the business data, after the business data is updated.
3. Save it in the city "after image", and finally generate a row lock.

The above operations are all completed within a database transaction, which ensures the atomicity of one-stage operations.

Phase two (success, failure)

1. If the second stage is successfully submitted.
Because the "business SQL" has been submitted to the database in the first stage, the Seata framework only needs to clean up the data with the snapshot data and row-level locks saved in the first stage.

2. The second stage failed (rollback)

If the second stage is a rollback, Seata needs to roll back the "business SQL" that has been executed in the first stage to restore the business data. The
rollback method is to restore the business data with the "before image"; but before the restoration, the dirty write must be checked first. , Comparing "database current business data" and "after image", if the two data are exactly the same, it means that there is no dirty writing, and the business data can be restored. If they are inconsistent, it means there is dirty writing, and dirty writing needs to be transferred to manual processing .

Guess you like

Origin blog.csdn.net/www1056481167/article/details/113700248