1, Distributed transaction problem
- A business operation needs to be called remotely across multiple data sources or across multiple systems, which will lead to distributed transaction problems.
give an example:
Business logic for users to purchase goods. The whole business logic is supported by three micro services:
- Warehousing service: deduct the warehousing quantity for a given commodity.
- Order service: create orders according to purchase requirements.
- Account service: deduct the balance from the user account.
The single application is split into micro service applications. The original three modules are split into three independent applications, which use 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.
2, Introduction to Seata
Seata official website address: http://seata.io/zh-cn/
- 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 will provide users with AT, TCC, SAGA and XA transaction modes to create a one-stop distributed solution for users.
Seata terminology:
-
TC (Transaction Coordinator) - Transaction Coordinator
Maintain the status of global and branch transactions and drive global transaction commit or rollback. -
TM (Transaction Manager) - transaction manager
Define the scope of global transaction: start global transaction, commit or roll back global transaction. -
RM (Resource Manager) - Resource Manager
Manage the resources of branch transaction processing, talk with TC to register branch transactions and report the status of branch transactions, and drive branch transaction commit or rollback.
Seata process:
- TM applies to TC to start a global transaction, which is successfully created and generates a globally unique XID;
- XID propagates in the context of microservice call link;
- RM registers branch transactions with TC and brings them under the jurisdiction of global transactions corresponding to XID;
- TM initiates a global commit or rollback resolution for XID to TC;
- TC schedules all branch transactions under XID to complete the commit or rollback request
3, Seata deployment
Version selection of Seata: Correspondence between Spring Cloud Alibaba, Spring Boot and Spring Cloud versions
Official Website Version Description
This time: Seata 1.2.0
3.1 configuration of Seata server
Official configuration document: Seata Deployment Guide
Download the source code of seata-server-1.2.0 and seata-1.2.0
- Seate server download: https://seata.io/zh-cn/blog/download.html
- seata-1.2.0 source code download: https://github.com/seata/seata/releases
3.1.1 modify configuration file
After decompression, enter the conf directory to start parameter configuration. We modify the file Conf and registry Conf these two files.
1. Enter the conf folder and modify the file Conf file
2. Modify registry Conf file
- If config is set to file, you do not need to set the configuration of nacos on the Internet
3.1.2 MySQL database configuration
We first create the database seata (the database should correspond to the db setting in file.conf), and the database table creation statement is in the server connection of README file
Let me execute mysql sql
-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS `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`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(96), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
3.1.3 start the Seata Server side
3.2 configuration of Seata client
3.2.1 business pre preparation
Business scenario: business logic for users to purchase goods. The whole business logic is supported by three micro services:
User A purchases goods, calls A service to create order completion, calls B service to deduct inventory, and then calls C service to deduct account balance. The data consistency within each service is guaranteed by local transactions, multiple service calls to complete the business, and the global transaction data consistency is guaranteed by Seata.
-
Order service A: create orders according to purchase requirements.
-
Warehousing service B: deduct the warehousing quantity for a given commodity.
-
Account Service C: deduct the balance from the user account.
**Business database preparation: * * configure three businesses corresponding to their respective databases.
- Database corresponding to service A: seata_order ; Table: t_order
- Database corresponding to service B: seata_storage ; Table: t_storage
- C service corresponding database: seata_account ; Table: t_account
Create table statement:
# Create seata_order database CREATE DATABASE seata_order; # Create t_order table CREATE TABLE seata_order.t_order( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` BIGINT(11) 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' ) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
# Create seata_storage database CREATE DATABASE seata_storage; # Create t_storage table CREATE TABLE seata_storage.t_storage( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `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' ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; # Insert a piece of data INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`) VALUES('1','1','100','0','100');
# Create seata_account database CREATE DATABASE seata_account; # Create t_account table CREATE TABLE seata_account.t_account( `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY 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 '0' COMMENT 'Remaining available limit' ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; # Insert a piece of data INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000');
3.2.2 creating undo_log table
Create the corresponding rollback log table according to the above three libraries, and the table creation file is in readme_ client in Zh file
undo_ The statement of creating the log table is as follows:
-- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id', `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME NOT NULL COMMENT 'create datetime', `log_modified` DATETIME NOT NULL COMMENT 'modify datetime', PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
Completion diagram:
The following configuration takes seata-order-service2001 as an example
-
Project structure:
3.2.3 Seata Client configuration file
Refer to readme for the preparation of configuration files_ client in Zh file:
1,file.conf, modify this
transport { # tcp, unix-domain-socket type = "TCP" #NIO, NATIVE server = "NIO" #enable heartbeat heartbeat = true # the tm client batch send request enable enableTmClientBatchSendRequest = false # the rm client batch send request enable enableRmClientBatchSendRequest = true # the rm client rpc request timeout rpcRmRequestTimeout = 2000 # the tm client rpc request timeout rpcTmRequestTimeout = 10000 # the tc client rpc request timeout rpcTcRequestTimeout = 5000 #thread factory for netty threadFactory { bossThreadPrefix = "NettyBoss" workerThreadPrefix = "NettyServerNIOWorker" serverExecutorThread-prefix = "NettyServerBizHandler" shareBossWorker = false clientSelectorThreadPrefix = "NettyClientSelector" clientSelectorThreadSize = 1 clientWorkerThreadPrefix = "NettyClientWorkerThread" # netty boss thread size bossThreadSize = 1 #auto default pin or 8 workerThreadSize = "default" } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #transaction service group mapping vgroupMapping.my_test_tx_group = "default" #only support when registry.type=file, please don't set multiple addresses default.grouplist = "127.0.0.1:8091" #degrade, current not support enableDegrade = false #disable seata disableGlobalTransaction = false } client { rm { asyncCommitBufferLimit = 10000 lock { retryInterval = 10 retryTimes = 30 retryPolicyBranchRollbackOnConflict = true } reportRetryCount = 5 tableMetaCheckEnable = false tableMetaCheckerInterval = 60000 reportSuccessEnable = false sagaBranchRegisterEnable = false sagaJsonParser = "fastjson" sagaRetryPersistModeUpdate = false sagaCompensatePersistModeUpdate = false tccActionInterceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000 } tm { commitRetryCount = 5 rollbackRetryCount = 5 defaultGlobalTransactionTimeout = 60000 degradeCheck = false degradeCheckPeriod = 2000 degradeCheckAllowTimes = 10 interceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000 } undo { dataValidation = true onlyCareUpdateColumns = true logSerialization = "jackson" logTable = "undo_log" compress { enable = true # allow zip, gzip, deflater, 7z, lz4, bzip2, zstd default is zip type = zip # if rollback info size > threshold, then will be compress # allow k m g t threshold = 64k } } loadBalance { type = "RandomLoadBalance" virtualNodes = 10 } } log { exceptionRate = 100 } tcc { fence { # tcc fence log table name logTableName = tcc_fence_log # tcc fence log clean period cleanPeriod = 1h } }
2,registry.conf, modify this
registry { # file ,nacos ,eureka,redis,zk,consul,etcd3,sofa,custom type = "nacos" nacos { application = "seata-server" serverAddr = "127.0.0.1:8848" namespace = "" username = "" password = "" ##if use MSE Nacos with auth, mutex with username/password attribute #accessKey = "" #secretKey = "" } eureka { serviceUrl = "http://localhost:8761/eureka" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" password = "" timeout = "0" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" aclToken = "" } etcd3 { serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } custom { name = "" } } config { # file,nacos ,apollo,zk,consul,etcd3,springCloudConfig,custom type = "file" nacos { serverAddr = "127.0.0.1:8848" namespace = "" group = "SEATA_GROUP" username = "" password = "" ##if use MSE Nacos with auth, mutex with username/password attribute #accessKey = "" #secretKey = "" dataId = "seata.properties" } consul { serverAddr = "127.0.0.1:8500" key = "seata.properties" aclToken = "" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" apolloAccesskeySecret = "" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" nodePath = "/seata/seata.properties" } etcd3 { serverAddr = "http://localhost:2379" key = "seata.properties" } file { name = "file.conf" } custom { name = "" } }
3.2.4 pom.xml file
<dependencies> <!--nacos--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--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>1.2.0</version> </dependency> <!--feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--web-actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--mysql-druid--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>
3.2.5 application.yml file
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: my_test_tx_group #Because seata's file There is no service module in the conf file, and the default transaction group name is my_test_tx_group #Service should be aligned with TX service group. vgroupMapping and grouplist are at the next level of service, my_test_tx_group is at the next level service: vgroupMapping: #Be consistent with the value of TX service group my_test_tx_group: default grouplist: # Address configuration of seata seaver. Cluster configuration here is an array default: 127.0.0.1:8091 nacos: discovery: server-addr: localhost:8848 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: root feign: hystrix: enabled: false logging: level: io: seata: mybatis: mapperLocations: classpath:mapper/*.xml
3.2.6 business code writing: add @ GlobalTransactional
The code of dao layer, mapper layer and controller layer is omitted
The business code uses OpenFeign to call between services to realize a simple business function. AT mode uses only one @ GlobalTransactional annotation to implement distributed transactions. The name attribute is a unique representation of transactions and can be defined AT will. The rollback for property specifies an Exception before the transaction is rolled back.
OrderServiceImpl.java
@Service @Slf4j public class OrderServiceImpl implements OrderService { @Resource private OrderDao orderDao; @Resource private StorageService storageService; @Resource private AccountService accountService; /** * Create order - > call inventory service to deduct inventory - > call account service to deduct account balance - > Modify order status * Simply put: * Place order - > reduce inventory - > reduce balance - > change status */ @Override @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class) public void create(Order order) { log.info("------->Order start"); //Create order for this application orderDao.create(order); //Remote call inventory service to deduct inventory log.info("------->order-service Start of inventory deduction"); storageService.decrease(order.getProductId(),order.getCount()); log.info("------->order-service End of inventory deduction"); //Remote call account service deduction balance log.info("------->order-service Start of deduction balance in"); accountService.decrease(order.getUserId(),order.getMoney()); log.info("------->order-service End of deduction balance in"); //Modify order status to completed log.info("------->order-service Start modifying order status in"); orderDao.update(order.getUserId(),0); log.info("------->order-service End of modifying order status in"); log.info("------->End of order"); } }
3.2.7 config code writing
1,MybatisConfig.java
@Configuration @MapperScan({"com.zb.springcloud.dao"}) public class MyBatisConfig { }
2,DataSourceProxyConfig.java
package com.zb.springcloud.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; @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(); } }
3.2.8 main startup
@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); } }
3.3 testing
-
Start the three services respectively. You can see that the Seata Sever server will give corresponding prompts during the project startup
Send request http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100 To simulate an order request. After sending multiple requests, you can see that the order has been placed successfully, and there are transactions in the process of processing on the Seata Server side. (at this time, there is no data in the database because the transaction is completed)
Reference blog: