Seata Architecture - AT Mode

foreword

Seata is an open source distributed transaction solution dedicated to providing high-performance and easy-to-use distributed transaction services. Seata will provide users with AT, TCC, SAGA and XA transaction modes to create a one-stop distributed solution for users.

X/Open DTP Model

X/Open DTP Model is a set of distributed transaction standards defined by X/Open organization, that is, it defines the specification and API interface of distributed transaction processing, but the specific implementation is implemented by each manufacturer.

image.png

TM and RM are integrated with the business system as the client of Seata, and TC is deployed independently as the server of Seata.

TC (Transaction Coordinator) - transaction coordinator

Maintain the state of global and branch transactions, and drive global transaction commit or rollback.

TM (Transaction Manager) - transaction manager

Manage global transactions, including starting global transactions, committing or rolling back global transactions.

RM (Resource Manager) - Resource Manager

Manage branch transactions, including registering branch transactions with TC and reporting the status of branch transactions, and driving branch transactions to commit or roll back.

In Seata, the execution flow of distributed transactions:

  • TM starts distributed transactions (TM registers global transaction records with TC)
  • Arrange transactional resources such as databases and services according to business scenarios (RM reports resource preparation status to TC)
  • TM ends the distributed transaction, and the transaction phase ends (TM notifies TC to commit/rollback the distributed transaction)
  • TC summarizes transaction information and decides whether to commit or roll back a distributed transaction
  • TC notifies all RMs to commit/rollback, and the second phase of the transaction ends

AT mode

overview

Seata AT mode is aNon-intrusive distributed transaction solution, Seata has built a proxy layer for database operations internally. When we use Seata AT mode, we actually use Seata's own data source proxy DataSourceProxy. Seata adds a lot of logic to this proxy layer, such as inserting and rolling back undo_log log, check global locks, etc.

Why check the global lock? This is because the transaction isolation of Seata AT mode is based on the local isolation level of the branch transaction. On the premise that the local isolation level of the database is read committed or above, Seata has designed a transaction coordination The global write exclusive lock maintained by the server ensures the write isolation between transactions. At the same time, the global transaction is defined at the read uncommitted isolation level by default.

Seata's transaction is a global transaction, which includes several branch local transactions. During the execution of the global transaction (the global transaction has not yet been executed), a local transaction is committed. If Seata does not take any measures, it will lead to The committed local transaction is read, resulting in dirty read, and if the data is modified by the local transaction committed before the global transaction is committed, it will cause dirty write.

Phase 1: Business data and rollback logs are committed in the same local transaction, and local locks and connection resources are released.

Phase 2: Committing is asynchronous and completes very quickly. Rollbacks are compensated by a one-phase rollback log reversal.

Interpretation of Seata AT mode isolation level

Applicable scene

  • Based on a relational database that supports native ACID transactions.
  • Java application, accessing the database through JDBC.

principle

first stage

1. Analyze SQL. Get SQL type (UPDATE), table (product), condition (where name = 'TXC') and other related information.

2. Mirror image before query: Generate query statements and locate data according to the condition information obtained through analysis.

select id, name, since from product where name = 'TXC';

Get the front image:

id name since
1 TXC 2014

3. Execute business SQL. Update the name of this record to 'GTS'.

4. Mirroring after query: According to the results of the previous mirroring , locate the data through the primary key.

After getting the mirror image:

id name since
1 GTS 2014

5. Insert rollback log: combine the front and rear mirror data and business SQL-related information into a rollback log record and insert it into UNDO_LOGthe table .

{
    
    
	"branchId": 641789253,
	"undoItems": [{
    
    
		"afterImage": {
    
    
			"rows": [{
    
    
				"fields": [{
    
    
					"name": "id",
					"type": 4,
					"value": 1
				}, {
    
    
					"name": "name",
					"type": 12,
					"value": "GTS"
				}, {
    
    
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"beforeImage": {
    
    
			"rows": [{
    
    
				"fields": [{
    
    
					"name": "id",
					"type": 4,
					"value": 1
				}, {
    
    
					"name": "name",
					"type": 12,
					"value": "TXC"
				}, {
    
    
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"sqlType": "UPDATE"
	}],
	"xid": "xid:xxx"
}

6. Before submitting, register the branch with TC: In the application productform , the global lock of the record whose primary key value is equal to 1 .

7. Local transaction submission: The update of business data is submitted together with the UNDO LOG generated in the previous steps.

8. Report the result of local transaction submission to TC.

second stage

rollback

1. After receiving the branch rollback request from TC, start a local transaction and perform the following operations.

2. Find the corresponding UNDO LOG record through XID and Branch ID.

3. Data verification: Compare the back mirror in the UNDO LOG with the current data. If there is a difference, it means that the data has been modified by an action other than the current global transaction. In this case, it needs to be handled according to the configuration policy, and the detailed description is introduced in another document.

4. Generate and execute the rollback statement according to the pre-image and business SQL information in the UNDO LOG:

update product set name = 'TXC' where id = 1;

5. Submit the local transaction. And report the execution result of the local transaction (that is, the result of the rollback of the branch transaction) to the TC.

submit

1. Receive the branch submission request from TC, put the request into an asynchronous task queue, and immediately return the successful submission result to TC.

2. The branch submission request in the asynchronous task stage will delete the corresponding UNDO LOG records asynchronously and in batches.

The rollback log table is as follows:

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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

write isolation

Before the local transaction is committed in the first stage, it is necessary to ensure that the global lock is obtained first, so that the local transaction can be committed.

If the attempt to acquire the global lock times out, the attempt is abandoned, and the local transaction is rolled back and the local lock is released.

Two global transactions tx1 and tx2 update the field m of table a respectively, and the initial value of m is 1000. tx1 starts first, starts the local transaction, acquires the local lock, and updates m = 1000 - 100 = 900. Before the local transaction is committed, the global , and the local commit releases the local lock. Start after tx2, start the local transaction, get the local lock, update operation m = 900 - 100 = 800. Before the local transaction is committed, try to get the global . Before tx1 is globally committed, the global lock of the record is held by tx1, and tx2 needs to retry to wait for the global lock .

tx1 two-phase global commit, release the global lock . tx2 gets the global lock and commits the local transaction.

insert image description here

If the two-stage global rollback of tx1, then tx1 needs to re-acquire the local lock of the data, and perform the update operation of reverse compensation to realize the rollback of the branch. At this point, if tx2 is still waiting for the global while holding the local lock, the branch rollback of tx1 will fail. The rollback of the branch will be retried until the lock such as the global lock of tx2 times out, the global and the local transaction is rolled back to release the local lock, and the branch rollback of tx1 is finally successful.

Because the global is held by tx1 until the end of tx1 , the problem of dirty writing will not occur.

insert image description here

read isolation

Based on the database local transaction isolation level of Read Committed (Read Committed) or above, the default global isolation level of Seata (AT mode) is Read Uncommitted (Read Uncommitted) .

insert image description here

The execution of the SELECT FOR UPDATE statement will apply for a global lock . If the global lock is held by another transaction, release the local lock (roll back the local execution of the SELECT FOR UPDATE statement) and try again. During this process, the query is blocked until the global lock is obtained, that is, the relevant data read is submitted , and then it will not return.

For overall performance considerations, Seata's current solution does not proxy all SELECT statements, but only for FOR UPDATE SELECT statements.

use

Suppose the Business service calls the Stock service and the Order service.

1. Configure the Business service.

  • Configure the applicationId and txServiceGroup (transaction grouping) of GlobalTransactionScanner.

  • Use the @GlobalTransactional annotation

  • You can use the RootContext.getXID() method to get the XID

  • Modify the contents of file.conf and registry.conf as needed

@Configuration
public class SeataAutoConfig {
    
    

    @Bean
    public GlobalTransactionScanner globalTransactionScanner() {
    
    
      	// 指定 applicationId、txServiceGroup
        return new GlobalTransactionScanner("dubbo-gts-seata-example", "my_test_tx_group");
    }
}
@Service
public class BusinessServiceImpl implements BusinessService {
    
    

    @Reference(version = "1.0.0")
    private StockDubboService stockDubboService;

    @Reference(version = "1.0.0")
    private OrderDubboService orderDubboService;

    private boolean flag;

    @Override
    @GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example")
    public ObjectResponse handleBusiness(BusinessDTO businessDTO) {
    
    
        System.out.println("开始全局事务,XID = " + RootContext.getXID());
        ObjectResponse<Object> objectResponse = new ObjectResponse<>();
        //1、扣减库存
        CommodityDTO commodityDTO = new CommodityDTO();
        commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
        commodityDTO.setCount(businessDTO.getCount());
        ObjectResponse stockResponse = stockDubboService.decreaseStock(commodityDTO);
        //2、创建订单
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setUserId(businessDTO.getUserId());
        orderDTO.setCommodityCode(businessDTO.getCommodityCode());
        orderDTO.setOrderCount(businessDTO.getCount());
        orderDTO.setOrderAmount(businessDTO.getAmount());
        ObjectResponse<OrderDTO> response = orderDubboService.createOrder(orderDTO);

        //打开注释测试事务发生异常后,全局回滚功能
        //  if (!flag) {
    
    
        //      throw new RuntimeException("测试抛异常后,分布式事务回滚!");
        //  }

        if (stockResponse.getStatus() != 200 || response.getStatus() != 200) {
    
    
            throw new DefaultException(RspStatusEnum.FAIL);
        }

        objectResponse.setStatus(RspStatusEnum.SUCCESS.getCode());
        objectResponse.setMessage(RspStatusEnum.SUCCESS.getMessage());
        objectResponse.setData(response.getData());
        return objectResponse;
    }
}

2. Configure the Stock service.

  • 配置 DataSource、DataSourceProxy、SqlSessionFactory、GlobalTransactionScanner
  • You can use the RootContext.getXID() method to get the XID
  • Modify the contents of file.conf and registry.conf as needed
  • Corresponding database configuration UNDO_LOG table
@Configuration
public class SeataAutoConfig {
    
    

    @Autowired
    private DataSourceProperties dataSourceProperties;

    @Bean
    @Primary
    public DruidDataSource druidDataSource() {
    
    
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(dataSourceProperties.getUrl());
        druidDataSource.setUsername(dataSourceProperties.getUsername());
        druidDataSource.setPassword(dataSourceProperties.getPassword());
        druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        druidDataSource.setInitialSize(0);
        druidDataSource.setMaxActive(180);
        druidDataSource.setMaxWait(60000);
        druidDataSource.setMinIdle(0);
        druidDataSource.setValidationQuery("Select 1 from DUAL");
        druidDataSource.setTestOnBorrow(false);
        druidDataSource.setTestOnReturn(false);
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
        druidDataSource.setMinEvictableIdleTimeMillis(25200000);
        druidDataSource.setRemoveAbandoned(true);
        druidDataSource.setRemoveAbandonedTimeout(1800);
        druidDataSource.setLogAbandoned(true);
        return druidDataSource;
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource) {
    
    
        return new DataSourceProxy(druidDataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
    
    
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSourceProxy);
        factoryBean.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*.xml"));
        factoryBean.setTransactionFactory(new JdbcTransactionFactory());
        return factoryBean.getObject();
    }

    @Bean
    public GlobalTransactionScanner globalTransactionScanner() {
    
    
        return new GlobalTransactionScanner("stock-gts-seata-example", "my_test_tx_group");
    }
}

3. Configure the Order service

  • 配置 DataSource、DataSourceProxy、SqlSessionFactory、GlobalTransactionScanner
  • You can use the RootContext.getXID() method to get the XID
  • Modify the contents of file.conf and registry.conf as needed
  • Corresponding database configuration UNDO_LOG table
@Configuration
public class SeataAutoConfig {
    
    

    @Autowired
    private DataSourceProperties dataSourceProperties;

    @Bean
    @Primary
    public DruidDataSource druidDataSource() {
    
    
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(dataSourceProperties.getUrl());
        druidDataSource.setUsername(dataSourceProperties.getUsername());
        druidDataSource.setPassword(dataSourceProperties.getPassword());
        druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        druidDataSource.setInitialSize(0);
        druidDataSource.setMaxActive(180);
        druidDataSource.setMaxWait(60000);
        druidDataSource.setMinIdle(0);
        druidDataSource.setValidationQuery("Select 1 from DUAL");
        druidDataSource.setTestOnBorrow(false);
        druidDataSource.setTestOnReturn(false);
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
        druidDataSource.setMinEvictableIdleTimeMillis(25200000);
        druidDataSource.setRemoveAbandoned(true);
        druidDataSource.setRemoveAbandonedTimeout(1800);
        druidDataSource.setLogAbandoned(true);
        return druidDataSource;
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource) {
    
    
        return new DataSourceProxy(druidDataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
    
    
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSourceProxy);
        factoryBean.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*.xml"));
        factoryBean.setTransactionFactory(new JdbcTransactionFactory());
        return factoryBean.getObject();
    }

    @Bean
    public GlobalTransactionScanner globalTransactionScanner() {
    
    
        return new GlobalTransactionScanner("order-gts-seata-example", "my_test_tx_group");
    }
}

Guess you like

Origin blog.csdn.net/qq_34561892/article/details/129098373