Spring Cloud Alibaba Seata handles distributed transactions

Posted by coreyp_1 on Mon, 07 Mar 2022 22:55:35 +0100

Spring Cloud Alibaba Seata handles distributed transactions

Distributed transaction management

preface

I don't want to post this blog, because the transaction rollback function is not perfectly implemented. If you see this blog, just understand it. If there is a solution in the future, I'll update it.

Distributed transaction

Before distribution, there is no such problem for single machine and single database, from 1:1 - > 1: n - > n: n

The unified scheduling of cross database and multi data sources will encounter the problem of distributed transactions

As shown in the figure below, the single application is split into micro service applications. The original three templates are split into three independent applications, using three independent data sources respectively. Business operations need to call three services to complete. At this time, the data consistency within each service is guaranteed by local transactions, but the global data consistency cannot be guaranteed.

Introduction to Seata

Official documents: Click me to transmit

Seata is an open source distributed transaction solution, which is committed to providing high-performance and easy-to-use distributed transaction services under the microservice architecture. Seata provides users with AT, TCC, SAGA and XA transaction modes to create a one-stop distributed solution for users.

Tell me about your understanding of Seata

Seata consists of a 1 + 3 suite

  • Transaction ID XID: globally unique transaction ID. as long as there are several libraries under the same ID, three are three and 10 are 10, indicating that they are a whole.
  • Of three components:
    • Transaction Coordinator (TC): Transaction Coordinator, which maintains global transactions and drives global transaction commit or rollback
    • Transaction Manager (TM): Transaction Manager, which defines the scope of global transactions, starts global transactions, commits or rolls back global transactions
    • Resource Manager (RM): resource manager, which manages the resources of branch transaction processing, talks with TC to register branch transactions and report the status of branch transactions, and drives branch transaction submission or rollback.

Processing process

  1. TM (transaction manager) applies to TC (Transaction Coordinator) to start a global transaction. The global transaction is created successfully and generates a XID (globally unique transaction ID)
  2. XID (globally unique transaction ID) is propagated in the context of the microservice invocation link
  3. RM (Resource Manager) registers branch transactions with TC (Transaction Coordinator) and brings them into the jurisdiction of global transactions corresponding to XID (globally unique transaction ID)
  4. TM (transaction manager) initiates a global commit or rollback resolution for XID (globally unique transaction ID) to TC (Transaction Coordinator)
  5. TM (transaction manager) schedules all branch transactions under XID (globally unique transaction ID) to complete the commit or rollback request

use

Spring comes with @ Transaction to control local transactions

The @ GlobalTransaction controls the global transaction

  • Local: @ Transaction
  • Global: @ GlobalTransaction

We only need to use this annotation on business classes that need to support distributed transactions

Microservices: it represents a group in a class. There are 20 people in a group

TC: Instructor

TM: head teacher

1. The head teacher initiates an application to the teaching teacher, whether the class can be started or not, and a class number will be created after the class is successfully started.

2. The head teacher informs each student of the class number in the group

3. All students report to the teacher through this class number, and the teacher adds them to the class

4. The head teacher asked the teacher to call the roll

5. After the roll call, the head teacher told the teacher that he could teach the students

Seata installation configuration

1. Download

Address: https://github.com/seata/seata/releases

After downloading version 0.9, modify the file in the conf directory Conf configuration file

2. Decompression

3. Modify file conf

First, we need to back up the original file Conf file, in the conf directory

It is mainly modified. The user-defined transaction group name + transaction log storage mode is db + database connection information, that is, modify the stored database

Modify the service module

Modify grouping in service module

Modify the store module

4. Create seata database

CREATE DATABASE `seata` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

Create a table in the seata database. The table creation statement is in DB under the seata/conf directory_ store. sql

5. Modify registry conf

The purpose is to specify the registration center as nacos and modify the nacos connection information

Then start the server and the NAS

6. Start

Start nacos first, then start seata: seata \ bin \ seata server bat

Order / inventory / Account Business micro service preparation

The following demonstrations need to start Nacos first, and then start Seata to ensure that both are OK

Business description of distributed transaction

Here we will create three micro services, one order service, one inventory service and one account service.

  • When the user places an order, an order will be created in the order service, and then the inventory of the ordered goods will be deducted by calling the inventory service remotely,
  • Then deduct the amount in the user's account by calling the account service remotely,
  • Finally, modify the order status to completed in the order service

This operation spans three databases and has two remote calls. It is obvious that there will be the problem of distributed transactions.

One sentence: place order - > deduct inventory - > deduct account (balance)

Create database

  • seata_order: the database that stores the order
  • seata_storage: a database that stores inventory
  • seata_account: the database that stores account information

Database building SQL

create database seata_order DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
create database seata_storage DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
create database seata_account DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

Create business table

-- seata_order Create under database t_order surface
use seata_order;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL COMMENT 'user id',
  `product_id` bigint(11) DEFAULT NULL COMMENT 'product id',
  `count` int(11) DEFAULT NULL COMMENT 'quantity',
  `money` decimal(11, 0) DEFAULT NULL COMMENT 'amount of money',
  `status` int(1) DEFAULT NULL COMMENT 'Order status:  0:Creating 1:Closed',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'Order form' ROW_FORMAT = Dynamic;

-- seata_storage Construction under reservoir t_storage surface
use seata_storage;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage`  (
  `id` bigint(11) NOT NULL AUTO_INCREMENT,
  `product_id` bigint(11) DEFAULT NULL COMMENT 'product id',
  `total` int(11) DEFAULT NULL COMMENT 'Total inventory',
  `used` int(11) DEFAULT NULL COMMENT 'Used inventory',
  `residue` int(11) DEFAULT NULL COMMENT 'Remaining inventory',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'stock' ROW_FORMAT = Dynamic;

INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);

-- seata_account Construction under reservoir t_account surface
use seata_account;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account`  (
  `id` bigint(11) NOT NULL COMMENT 'id',
  `user_id` bigint(11) DEFAULT NULL COMMENT 'user id',
  `total` decimal(10, 0) DEFAULT NULL COMMENT 'Total amount',
  `used` decimal(10, 0) DEFAULT NULL COMMENT 'Balance used',
  `residue` decimal(10, 0) DEFAULT NULL COMMENT 'Remaining available limit',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'Account table' ROW_FORMAT = Dynamic;
 
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);

Create rollback log table

Each of the three databases of order inventory account needs to build its own rollback log table. The location of SQL file is seata\conf\db_undo_log.sql

-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- This script must be initialized in your current business database for AT pattern XID record. And server End independent (Note: business database)
-- Note here 0.3.0+ Add unique index ux_undo_log
-- Open the following use Note, you can quickly create a table
--use seata_order;
--use seata_storage;
--use seata_account;
drop table if exists `undo_log`;
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;

Order / inventory / Account Business micro service preparation

Business requirements

Place order - > reduce inventory - > deduct balance - > Change (order) status

Create Seata service module 2001

appointment

Entity, domain: equivalent to entity class layer

vo: view object´╝îvalue object

dto: data transmission class from foreground to background

Introducing POM

		<!--seata-->
		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
			<exclusions>
				<exclusion>
					<artifactId>seata-all</artifactId>
					<groupId>io.seata</groupId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>io.seata</groupId>
			<artifactId>seata-all</artifactId>
			<version>0.9.0</version>
		</dependency>

		<!--nacos-->
		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
		</dependency>

		<!--openfeign-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>

		<!--boot: web actuator-->

		<!--Common configuration without introducing hot deployment-->

		<!--Database configuration-->

Modify yml

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        #The user-defined transaction group name needs to correspond to that in Seata server
        tx-service-group: fsp_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: 123456

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

Add file conf

In the resources directory, create file Conf file

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}

service {

  vgroup_mapping.fsp_tx_group = "default" #Modify custom transaction group name

  default.grouplist = "127.0.0.1:8091"
  enableDegrade = false
  disable = false
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
  disableGlobalTransaction = false
}


client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
}

## transaction log store
store {
  ## store mode: file,db
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## 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
  }
}
lock {
  ## the lock store mode: local,remote
  mode = "remote"

  local {
    ## store locks in user's database
  }

  remote {
    ## store locks in the seata's server
  }
}
recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

support {
  ## spring
  spring {
    # auto proxy the DataSource bean
    datasource.autoproxy = false
  }
}

registry.conf

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

  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file,nacos ,apollo,zk,consul,etcd3
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

domain

Order.java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status; // Order status: 0: being created, 1: closed
}

CommonResult.java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult<T> {
    private Integer code;
    private String message;
    private  T data;

    public CommonResult(Integer code, String message) {
        this(code,message,null);
    }
}

Dao interface and its implementation

OrderDao.java

@Mapper
public interface OrderDao {
    void create(Order order);
    void update(@Param("userId") Long userId, @Param("status") Integer status);
}

OrderMapper.xml, create a mapper folder under the Resources folder and put it under the mapper folder

<?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.indi.springcloud.alibaba.dao.OrderDao">
	<resultMap id="BaseResultMap" type="com.indi.springcloud.alibaba.domain.Order">
		<id column="id" property="id" jdbcType="BIGINT"/>
		<result column="user_id" property="userId" jdbcType="BIGINT"/>
		<result column="product_id" property="productId" jdbcType="BIGINT"/>
		<result column="count" property="count" jdbcType="INTEGER"/>
		<result column="money" property="money" jdbcType="DECIMAL"/>
		<result column="status" property="status" jdbcType="INTEGER"/>
	</resultMap>
	<insert id="create">
		insert into t_order (id, user_id,product_id,count,money,status)
		values(null, #{userId},#{productId},#{count},#{money},0);
	</insert>

	<update id="update">
		update t_order set status = 1
		where user_id = #{userId} and status = #{status}
	</update>
</mapper>

Service implementation class

OrderService.java, the orderservice here is an empty shell, and the specific operation is implemented by its implementation class.

public interface OrderService {
    void create(Order order);
}

StorageService.java

@FeignClient(value = "seata-storage-service")
public interface StorageService {
    @PostMapping("/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

AccountService.java

@FeignClient(value = "seata-account-service")
public interface AccountService {
    @PostMapping("/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

OrderServiceImpl.java implementation class

/**
 * Place order - > reduce inventory - > reduce balance - > change status
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderDao orderDao;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;

    @Override
    public void create(Order order) {
        log.info("----->Start new order");
        orderDao.create(order);

        log.info("----->The order micro service starts to call inventory and deduct Count");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("----->End of inventory deduction");

        log.info("----->The order micro service starts to call the account and deduct Money");
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("----->End of account deduction");

        // Modify the order status from 0 to 1, and 1 means it has been completed
        log.info("Start modifying order status");
        orderDao.update(order.getUserId(), 0);
        log.info("End of modifying order status");

        log.info("Order placement completed");
    }
}

controller

@RestController
public class OrderController {
    @Resource
    private OrderService orderService;

    @GetMapping("/order/create")
    public CommonResult create(Order order){
        orderService.create(order);
        return new CommonResult(200, "Order created successfully");
    }
}

Config configuration

MyBatisConfig.java

@Configuration
@MapperScan({"com.indi.springcloud.alibaba.dao"})
public class MyBatisConfig {

}

DataSourceProxyConfig.java here is to use Seata to proxy the data source

package com.indi.springcloud.alibaba.config;


import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * Proxy data sources using Seata
 */
@Configuration
public class DataSourceProxyConfig {
    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }
}

Startup class

SeataOrderMainApp2001.java

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // Cancel automatic creation of data source
public class SeataOrderMainApp2001 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class,args);
    }
}

New Seata storage service2002

yml

domain

Storage.java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Storage {
    private Long id;
    private Long productId;
    private Integer total;
    private Integer used;
    private Integer residue;
}

CommonResult.java

Same as 2001

Dao interface and its implementation

StorageDao.java

@Mapper
public interface StorageDao {
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

StorageMapper.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.indi.springcloud.alibaba.dao.StorageDao">
	<resultMap id="BaseResultmap" type="com.indi.springcloud.alibaba.domain.Storage">
		<id property="id" column="id" jdbcType="BIGINT"></id>
		<result property="productId" column="product_id" jdbcType="BIGINT"></result>
		<result property="total" column="total" jdbcType="INTEGER"></result>
		<result property="used" column="used" jdbcType="INTEGER"></result>
		<result property="residue" column="residue" jdbcType="INTEGER"></result>
	</resultMap>
	<update id="decrease">
		update
			t_storage
		set
			used = used + #{count}, residue = residue - #{count}
		where
			product_id = #{productId}
	</update>
</mapper>

Service implementation class

StorageService.java

public interface StorageService {
    void decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

StorageServiceImpl.java implementation class

@Service
public class StorageServiceImpl implements StorageService {
    private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);

    @Resource
    private StorageDao storageDao;

    @Override
    public void decrease(Long productId, Integer count) {
        LOGGER.info("------storage-service Start deducting inventory");
        storageDao.decrease(productId,count);
        LOGGER.info("------storage-service End of inventory deduction");
    }
}

controller

@RestController
public class StorageController {
    @Resource
    private StorageService storageService;

    @RequestMapping("/storage/decrease")
    public CommonResult decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count){
        storageService.decrease(productId,count);
        return new CommonResult(200, "Inventory deduction succeeded");
    }
}

Startup class

Same as 2001

New Seata account service2003

yml

domain

Account.java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private Long id;
    private Long userId;
    private BigDecimal total;
    private BigDecimal used;
    private BigDecimal residue;
}

CommonResult.java

Same as 2001

Dao

AccountDao.java

@Mapper
public interface AccountDao {
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

AccountMapper.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.indi.springcloud.alibaba.dao.AccountDao">
	<resultMap id="BaseResultMap" type="com.indi.springcloud.alibaba.domain.Account">
		<id property="id" column="id" jdbcType="BIGINT"/>
		<result property="userId" column="user_id" jdbcType="BIGINT"/>
		<result property="total" column="total" jdbcType="DECIMAL"/>
		<result property="used" column="used" jdbcType="DECIMAL"/>
		<result property="residue" column="residue" jdbcType="DECIMAL"/>
	</resultMap>
	<update id="decrease">
		update
			t_account
		set
			used = used + #{money} , residue = residue + #{money}
		where
			user_id = #{userId}
	</update>
</mapper>

Service

AccountService.java

public interface AccountService {
    void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

AccountServiceImpl.java

@Service
public class AccountServiceImpl implements AccountService {
    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Resource
    private AccountDao accountDao;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        LOGGER.info("------account-service Start deducting balance");
        accountDao.decrease(userId, money);
        LOGGER.info("------account-service End of deduction balance");
    }
}`

Controller

AccountController.java

@RestController
public class AccountController {
    @Resource
    private AccountService accountService;

    @RequestMapping("/account/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
        accountService.decrease(userId,money);
        return new CommonResult(200,"Balance deducted successfully");
    }
}

Startup class

Same as 2001

test

Initial condition of database

Normal order

visit

http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

Timeout exception, no @ GlobalTransaction added

In the 2003 module, we added a sleep time of 20 seconds. The default time of openFeign is 1 second, so we will report timeout exception

Fault condition

After the order is created, the inventory is deducted and the balance is deducted. When the inventory and account amount are deducted, the order status is not changed from 0 to 1 and set to completed

Moreover, due to Feign's retry mechanism, the account balance may be deducted many times

Timeout exception, add @ GlobalTransaction

Modify orderserviceimpl. Of 2001 module java

@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)

name can start at will, as long as it does not conflict with other configurations

Rollback for indicates that any error will be rolled back

After adding this, the error is still reported, but the database after placing the order has not changed. That is the ideal state. In fact, only the order database has not changed, and both storage and account have changed. This problem has not been solved. If you have a way, please comment below. Thank you very much!

Part supplement

Seata

In January 2019, ant financial services and Alibaba jointly launched an open source distributed transaction solution

Seata: Simple Extensible Autonomous Transaction Architecture

Starting from 2020, the version after 1.0 will be used after working.

Let's look at the three components of TC/TM/RM

What is TC, TM, RM

TC: seata server

TM: method with @ GlobalTransaction annotation

RM: database, that is, transaction participants

Execution flow of distributed transactions

  • TM starts the distributed transaction (TM registers the global transaction record with TC), which is equivalent to the annotation @ globaltransaction annotation
  • Arrange internal resources of database, service and other transactions according to business scenarios (RM reports resource preparation status to TC)
  • TM ends the distributed transaction, and the first phase of the transaction ends (TM notifies TC to commit and roll back the distributed transaction)
  • TC summarizes transaction information and determines whether distributed transactions are committed or rolled back
  • TC notifies all RM S to commit and roll back resources, and the second phase of the transaction ends

How does AT mode achieve no intrusion into business

The default AT mode is Alibaba cloud GTS

AT mode

premise

  • Based on relational database supporting local ACID transactions
  • Java application, accessing database through JDBC

Overall mechanism

Evolution of two-stage submission agreement

  • Phase I: the business data and rollback log records are committed in the same local transaction, and the local lock and connection resources are released
  • Second stage
    • Asynchronous submission, very fast completion
    • Rollback makes reverse compensation through the rollback log of one stage

One stage loading

In the first phase, Seata intercepts business SQL

  • Analyze the SQL semantics, find the business SQL, and save the business data to be updated as a before image (pre image) before the business data is updated
  • Execute business SQL to update business data. After the business data is updated
  • Save it as after image, and finally generate row lock

All the above operations are completed in one database transaction, which ensures the atomicity of one-stage operation

Phase II submission

If the second phase is successfully submitted, because the business SQL has been submitted to the database in the first phase, the Seata framework only needs to delete the snapshot and row lock saved in the first phase and complete the data cleaning

Two phase rollback

If the second phase is rolled back, Seata needs to roll back to the business SQL executed in the first phase to restore the business data

The rollback method is to restore the business data with before image. However, before restoring, the dirty write must be verified first. Compare the current business data of the database with the after image. If the two data are completely consistent and there is no dirty write, the business data can be restored. If the two data are inconsistent, it indicates that there is dirty read, and the dirty read needs to be transferred to manual processing

summary

Topics: Spring Cloud seata