(20) SpringCloud Alibaba Seata handles distributed transactions
1. Distributed problems
Distributed before:
- Single machine single library does not have this problem
- From 1:1 -> 1:N -> N: N
After distributed:
In a word: Distributed transaction problems arise when a business operation needs to be called remotely across multiple data sources or across multiple systems
2. Introduction to Seata
2.1 what is
Seata is an open source distributed transaction solution dedicated to providing high performance and easy to use distributed transaction services under the microservices architecture
Official website: seata. IO /zh-cn/
2.2 can do
A typical distributed transaction process
— ID+ three component model for distributed transaction processing
Transaction ID XID: globally unique Transaction ID
3 Component Concepts:
- Transaction Coordinator(TC) : a Transaction Coordinator who maintains the running status of global transactions and coordinates and drives the submission or rollback of global transactions.
- Transaction Manager(TM) : Controls the boundaries of a global Transaction, is responsible for starting a global Transaction, and ultimately initiating a global commit or rollback resolution;
- Resource Manager(RM) : Controls branch transactions, is responsible for branch registration, status reporting, receives instructions from transaction coordinator, and drives the submission and rollback of branch (local) transactions;
2.3 to the next
Release note :github.com/seata/seata…
2.4 how to play
Spring local @ Transactional
Global @GlobalTransactional: SEATA’s distributed transaction solution
3. Seata-server installation
3.1 Official website address
Seata. IO/useful – cn/blog /…
3.2 Download Version
Seata – server – 0.9.0
3.3 unzip
Decompress seata-server-0.9.0.zip to the specified directory and modify the file.conf configuration file in the conf directory
-
Back up the original file.conf file
-
Major changes: Set the transaction group name + transaction log storage mode to DB + database connection information
-
File. The conf modification:
-
Service module: vgroup_mapping.my_test_tx_group = “fsp_tx_group”
-
Store module:
mode = “db”
Url = “JDBC: mysql: / / 127.0.0.1:3306 / seata” user = “root”, “=” your password”
-
3.4 mysql5.7 Creating a new database seata
3.5 Build tables in seATA database
Create table db_store. SQL in \seata-server-0.9.0\seata\conf
Set db_store. SQL and paste it into SQLYog
3.6 Modifying the Registry. Conf configuration file
Modify the registry. Conf configuration file in the seata-server-0.9.0\seata\conf directory
Registry {#file, NACOS, Eureka, Redis, ZK, Consul, ETCD3, SOFA type ="nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
Copy the code
3.7 Starting Nacos First Port 8848
3.8 Starting seata-server again
Go to the directory softs\seata-server-0.9.0\seata\bin, run CMD, type seata-server.bat
4. Order/inventory/account business database preparation
4.1 illustrates
The following demonstrations require that Nacos be started before Seata, and both are OK
No available server to connect
4.2 Distributed Transaction Service Description
Place order –> Deduct stock –> Reduce account (balance)
4.3 Creating a Service Database
- Seata_order: Database to store orders
- Seata_storage: database for storing inventory
- Seata_account: database for storing account information
4.3 Establish corresponding service tables according to the above three databases
-
Create a T_ORDER table under seatA_ORDER
CREATE TABLE 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 'number', `money` DECIMAL(11.0) DEFAULT NULL COMMENT 'value', `status` INT(1) DEFAULT NULL COMMENT 'Order status: 0: Creating; 1: Finished ' ) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; SELECT * FROM t_order; Copy the code
-
Create a T_storage table for the seatA_storage database
CREATE TABLE 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 'The total inventory', `used` INT(11) DEFAULT NULL COMMENT 'Have used inventory', `residue` INT(11) DEFAULT NULL COMMENT 'The remaining inventory' ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`) VALUES('1', '1', '100', '0', '100'); SELECT * FROM t_storage;Copy the code
-
Create a T_account table under the seatA_Account library
CREATE TABLE 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 'Used balance', `residue` DECIMAL(10.0) DEFAULT '0' COMMENT 'Remaining available amount' ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1'.'1'.'1000'.'0'.'1000') SELECT * FROM t_account; Copy the code
4.4 Create rollback log tables based on the preceding three databases
Order – Inventory – account three libraries need to build their own rollback log table
Paste db_undo_log.sql in the \seata-server-0.9.0\seata\conf directory
Then create a new table in each database
4.5 Final Effect
5. Order/inventory/account business micro-service preparation
5.1 Service Requirements
Place order -> reduce inventory -> deduct balance -> change (order) status
5.2 Creating an Order order-module
5.2.1 the new model
seata-order-service2001
5.2.2 POM
<dependencies>
<! --nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<! -- Seata package needs to be removed due to compatibility issues.
<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>
<! --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>
<! --spring cloud alibaba nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<! --SpringCloud Alibaba Sentinel -- Datasource -- nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<! --SpringCloud alibaba Sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<! --mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<! Mybatis and SpringBoot integration -->
<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>
Copy the code
5.2.3 requires YML
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
Set transaction group name to 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? useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT
username: root
password: root
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
Copy the code
5.2.4 file. Conf
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" Default. Grouplist = "127.0.0.1:8091" enableDegrade = false disable = false max.mit.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: Branch session size = branch session size = "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 ", "= = "root" 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 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 } }Copy the code
5.2.5 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"} 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 = Zk "http://192.168.1.204:8801"} {serverAddr = "127.0.0.1:2181" session. Timeout = 6000 connect timeout = 2000} etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }Copy the code
5.2.6 domain
Order type:
package com.atguigu.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order
{
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; // Order status: 0: creating; 1: Finished
}
Copy the code
CommonResult class:
package com.atguigu.springcloud.alibaba.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null); }}Copy the code
5.2.7 Dao interface and implementation
package com.atguigu.springcloud.alibaba.dao;
import com.atguigu.springcloud.alibaba.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderDao {
// Create an order
void create(Order order);
// Change the order status from zero to 1
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
Copy the code
Create a new mapper folder under resources and add ordermapper.xml
<! DOCTYPEmapper PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.springcloud.alibaba.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.atguigu.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>
Copy the code
5.2.8 Service Interface and Implementation
OrderService:
package com.atguigu.springcloud.alibaba.service;
import com.atguigu.springcloud.alibaba.domain.Order;
public interface OrderService {
// Create an order
void create(Order order);
}
Copy the code
AccountService:
package com.atguigu.springcloud.alibaba.service;
import com.atguigu.springcloud.alibaba.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.math.BigDecimal;
@FeignClient(value = "seata-account-service")
public interface AccountService {
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId")Long userId,
@RequestParam("money")BigDecimal money);
}
Copy the code
StorageService:
package com.atguigu.springcloud.alibaba.service;
import com.atguigu.springcloud.alibaba.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "seata-storage-service")// Invoke the inventory microservice
public interface StorageService {
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count);
}
Copy the code
OrderServiceImpl:
package com.atguigu.springcloud.alibaba.service.impl;
import com.atguigu.springcloud.alibaba.dao.OrderDao;
import com.atguigu.springcloud.alibaba.domain.Order;
import com.atguigu.springcloud.alibaba.service.AccountService;
import com.atguigu.springcloud.alibaba.service.OrderService;
import com.atguigu.springcloud.alibaba.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@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 */
@GlobalTransactional
@Override
public void create(Order order) {
System.out.println("--------> Start new order");
// Create an order
orderDao.create(order);
// Deduct inventory
System.out.println("----> Order microservice starts calling inventory, making deduction Count");
storageService.decrease(order.getProductId(),order.getCount());
System.out.println("----> Order microservice starts calling inventory, making deductions end");
// Make a deduction account
System.out.println("-- > Order micro service starts to call account, do deduction Money");
accountService.decrease(order.getUserId(),order.getMoney());
System.out.println("-- > Order microservice call account, make deduction end");
// Change the order status from 0 to 1
System.out.println("-- > Start modifying order status");
orderDao.update(order.getUserId(),0);
System.out.println("-- > Modify order status end (#^.^#)"); }}Copy the code
5.2.9 Controller
package com.atguigu.springcloud.alibaba.controller;
import com.atguigu.springcloud.alibaba.domain.CommonResult;
import com.atguigu.springcloud.alibaba.domain.Order;
import com.atguigu.springcloud.alibaba.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;
import javax.smartcardio.CommandAPDU;
@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");
}
@RequestMapping("/order/test")
public String test(a){
System.out.println("test 2001");
return "test 2001"; }}Copy the code
5.2.10 Config configuration
MyBatisConfig:
package com.atguigu.springcloud.alibaba.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {}Copy the code
DataSourceProxyConfig:
package com.atguigu.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;
/** * Use Seata to proxy data sources */
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(a){
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());
returnsqlSessionFactoryBean.getObject(); }}Copy the code
5.2.11 Primary Boot Class
package com.atguigu.springcloud.alibaba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)// Unconfigure the data source automatically and use Seata to proxy the data source
public class SeataOrderMainApp2001 {
public static void main(String[] args) { SpringApplication.run(SeataOrderMainApp2001.class,args); }}Copy the code
www.freesion.com/article/755…
no available server to connect.
5.3 Creating an Inventory storage-Module
5.4 Creating an Account-module Account
6, the Test
6.1 Database status before test:
6.2 access:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
6.3 Database Status After Access
6.4 Background:
2001
2002:
2003:
6.4Transactional @globalTransactional:
-
AccountServiceImpl Adds a timeout
public void decrease(Long userId, BigDecimal money) { System.out.println(("-------> Account balance deduction begins in account-service")); // Simulate timeout exception, global transaction rollback try { TimeUnit.SECONDS.sleep(20); }catch (InterruptedException e) { e.printStackTrace(); } accountDao.decrease(userId,money); System.out.println("-------> End of account deduction in account-service"); } Copy the code
-
Restart 2003 and visit again
-
Database situation
-
Fault condition
- When inventory and account balance are deducted, the order status is not set to completed and is not changed from zero to 1
- And because of Feign’s retry mechanism, the account balance can be deducted multiple times
6.5 If timeout fails, add @globalTransactional
-
AccountServiceImpl Adds a timeout
-
OrderServiceImpl
@GlobalTransactional
-
Test again:
-
View database:
- After placing an order, the database data does not change, and records cannot be added
7. Introduction to the principle of Seata
7.1 Seata
Simple Extensible Autonomous Transaction Architecture: A Simple Extensible Autonomous Transaction Architecture
2020 At first, use version 1.0 or later after entering the workforce
7.2 TC, TM, and RM Components
TC: seATA server
TM: The initiator of things, the gateway to business. @GlobalTransactional(name = “txl-create-order”, rollbackFor = Exception.class)
RM: The participant of a transaction. A database is an RM.
The execution flow of distributed transactions:
- TM enables distributed transactions (TM registers global transaction records with TC)
- Change service scenarios, arrange intra-transaction resources such as databases and services (RM reports resource readiness status to TC)
- TM ends distributed transaction, transaction end of phase 1 (TM notifies TC to commit/roll back distributed transaction)
- The TC summarizes the transaction information and decides whether a distributed transaction should be committed or rolled back
- The TC notifies all RM to commit or roll back resources, and the transaction phase 2 ends.
7.3 How does THE AT Mode Prevent Service Intrusion
1) What is it
2) One-stage loading
3) Two-stage submission
4) Two-stage rollback