Distributed transaction solution---Seata

1. Distributed transactions

2. Common solutions for distributed transactions

Distributed transaction solutions usually include the following:

Two-phase commit (2PC): It is a classic distributed transaction solution that divides distributed transactions into two phases: preparation and submission, and implements distributed transactions through coordinators (Coordinator) and participants (Participant) consistency. However, due to the strong consistency and blocking characteristics of 2PC, the performance is poor in high concurrency scenarios.

TCC (Try-Confirm-Cancel): It is a distributed transaction solution based on business logic, which achieves the consistency of distributed transactions through the try, confirm and cancel phases of the transaction. The implementation of TCC requires splitting and reconstructing business logic, which is relatively complex.

Message queue: achieves consistency in distributed transactions by splitting them into asynchronous messages. The specific implementation method is to put each sub-operation of the distributed transaction into the message queue, and ensure the consistency of the distributed transaction through the transactional characteristics of the message queue. The implementation of message queue is relatively simple, but the performance and reliability requirements of message queue are relatively high.

Seata: It is a new type of distributed transaction solution that achieves the consistency of distributed transactions through the division, coordination and compensation of distributed transactions. Seata supports multiple transaction modes and transaction log storage methods, which can adapt to various business scenarios.

The above are several common distributed transaction solutions. Each solution has its advantages, disadvantages and applicable scenarios. The specific solution to choose needs to be evaluated according to the specific business scenario.
The following mainly introduces the commonly used message correcting and Seata methods

3. Message queue

When using message queues as a distributed transaction solution, you can split a distributed transaction into multiple sub-operations, each sub-operation is an independent message, and then put these messages into the message queue. The transactional nature of the message queue can ensure the reliability of the message, that is, only when the message is successfully consumed, it will be removed from the message queue, thereby ensuring that the message is not lost and is not repeatedly consumed.

For example, for an e-commerce order scenario, the distributed transaction can be split into the following sub-operations:
1. Reduce product inventory.
2. Generate an order.
3. Deduct the user account balance.

For these sub-operations, you can convert them into messages respectively, and then put these messages into the message queue. For each sub-operation, you can use the transactional features provided by Message Queuing to ensure their consistency. If any sub-operation fails, the distributed transaction can be rolled back by rolling back the sent messages in the message queue.

By using message queues as a solution for distributed transactions, the locking time of distributed transactions can be shortened, thereby improving the performance and throughput of distributed transactions. At the same time, the message queue has good scalability and reliability and can cope with various challenges in high concurrency scenarios.

The following is an example of a distributed transaction implemented based on RocketMQ to demonstrate how to split a distributed transaction into multiple sub-operations and convert them into messages in an e-commerce order scenario, and then send these messages to RocketMQ. Finally, the consistency of distributed transactions is achieved through RocketMQ's transaction message features.

First, we need to define a RocketMQ transaction listener (TransactionListener) to monitor the transaction status and implement transaction commit and rollback operations. Here is an example RocketMQ transaction listener:

public class OrderTransactionListener implements TransactionListener {
    
    

    // 模拟商品库存和用户账户余额
    private Map<String, Integer> productInventory = new HashMap<>();
    private Map<String, Integer> userBalance = new HashMap<>();

    // 初始化商品库存和用户账户余额
    public OrderTransactionListener() {
    
    
        productInventory.put("product1", 100);
        productInventory.put("product2", 200);
        userBalance.put("user1", 1000);
        userBalance.put("user2", 2000);
    }

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
    
    
        try {
    
    
            // 解析消息中的订单信息
            OrderInfo order = JSON.parseObject(new String((byte[])msg.getBody()), OrderInfo.class);
            // 减少商品库存
            reduceProductInventory(order.getProduct(), order.getQuantity());
            // 扣除用户账户余额
            reduceUserBalance(order.getUser(), order.getTotalPrice());
            // 订单生成成功,返回COMMIT_MESSAGE状态
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
    
    
            // 出现异常,返回ROLLBACK_MESSAGE状态
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
    
    
        // 根据消息中的订单信息检查订单状态,如果订单已经生成,则返回COMMIT_MESSAGE状态,否则返回ROLLBACK_MESSAGE状态
        OrderInfo order = JSON.parseObject(new String(msg.getBody()), OrderInfo.class);
        if (order != null && order.getStatus() == OrderStatus.CREATED) {
    
    
            return LocalTransactionState.COMMIT_MESSAGE;
        } else {
    
    
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    // 减少商品库存
    private void reduceProductInventory(String productId, int quantity) {
    
    
        Integer inventory = productInventory.get(productId);
        if (inventory == null) {
    
    
            inventory = 0;
        }
        inventory -= quantity;
        productInventory.put(productId, inventory);
    }

    // 扣除用户账户余额
    private void reduceUserBalance(String userId, BigDecimal amount) {
    
    
        Integer balance = userBalance.get(userId);
        if (balance == null) {
    
    
            balance = 0;
        }
        balance -= amount.intValue();
        userBalance.put(userId, balance);
    }
}

In the above code, we define an OrderTransactionListener class and implement RocketMQ's transaction listener interface (TransactionListener). In the executeLocalTransaction method, we store the product information and user information in the received order message into the local product inventory and user account balance, and return the LocalTransactionState.COMMIT_MESSAGE status at the end of the method. In the checkLocalTransaction method, we check whether the order status is generated, if so, return the LocalTransactionState.COMMIT_MESSAGE status, otherwise return the LocalTransactionState.ROLLBACK_MESSAGE status.

Next, we need to convert the order generation operation into a transaction message and send it to RocketMQ. The following is an example order generation service (OrderService):

public class OrderService {
    
    

    private DefaultMQProducer producer;

    public OrderService() throws MQClientException {
    
    
        // 创建生产者实例,并设置事务监听器
        producer = new DefaultMQProducer("producerGroup");
        producer.setTransactionListener(new OrderTransactionListener());
        // 启动生产者实例
        producer.start();
    }

    public void createOrder(OrderInfo order) throws MQClientException, InterruptedException {
    
    
        // 创建订单消息
        Message message = new Message("orderTopic", "orderTag", JSON.toJSONBytes(order));
        // 发送事务消息
        TransactionSendResult result = producer.sendMessageInTransaction(message, null);
        // 根据事务消息发送结果,判断是否发送成功
        if (result.getLocalTransactionState() != LocalTransactionState.COMMIT_MESSAGE) {
    
    
            throw new RuntimeException("Failed to send transaction message");
        }
    }
}

In the above code, we define an OrderService class, in which the createOrder method receives an order information object, converts it into an order message and sends it to RocketMQ. When sending, we call the sendMessageInTransaction method of the RocketMQ producer instance and pass in the order message and transaction execution parameters (null in this case). This method will return a TransactionSendResult object, which contains the sending result and status of the transaction message. If the sending result is LocalTransactionState.COMMIT_MESSAGE, it means that the transaction message was sent successfully; otherwise, it means that the sending failed, and we throw a runtime exception here.

Through the above examples, we can see how to use RocketMQ's transaction message feature to achieve the consistency of distributed transactions. In this example, we split the order generation operation into two sub-operations: reducing product inventory and deducting user account balance, and then convert these sub-operations into two transaction messages and send them to RocketMQ, guaranteed by the implementation of the transaction listener Distributed transaction consistency. When all sub-operations are executed successfully, RocketMQ will submit the transaction and formally send the order generation message; otherwise, RocketMQ will roll back the transaction and cancel all sub-operations to ensure the consistency of distributed transactions.

4、Set

Taking an e-commerce project as an example, we will introduce Seata's role in distributed transaction management in more detail.

In a typical e-commerce project, the following microservices may be included:

1. Order service: Responsible for processing user orders, order inquiries and other operations.
2. Inventory service: Responsible for handling product inventory, inventory reduction and other operations.
3. Payment service: Responsible for processing user payments, querying payment status and other operations.

In this project, the order operation requires the participation of three microservices: order service, inventory service and payment service. Here is a simple process:

1. The user's order request reaches the order service, which generates the order and calls the inventory service to deduct inventory.
2. After the inventory service successfully deducts the inventory, the payment service is called for payment.
3. After the payment service processes the payment, the order status is updated to paid.

In this process, transaction operations between the three microservices need to be coordinated to ensure data consistency and integrity. If an exception occurs in one of the services, the operations of all microservices need to be rolled back to ensure data consistency and integrity.

This kind of distributed transaction management and coordination can be achieved using Seata. Specifically, Seata components can be embedded in order services, inventory services, and payment services, and transaction operations between various microservices can be coordinated through Seata components. In the example above, you can use Seata to do the following:

1. The order service initiates a distributed transaction: When the user's order request reaches the order service, the order service uses Seata's API to initiate a distributed transaction, and at the same time adds the inventory service and payment service to the distributed transaction as participants. middle.
2. The inventory service and payment service are registered as branch transactions: When the inventory service and payment service receive a request from the order service, they also use Seata's API to register as a branch transaction and register the branch transaction as The information is returned to the order service.
3. Seata component coordinates distributed transactions: During the execution of distributed transactions, Seata components will coordinate the transaction operations of each participant. For example, if an exception occurs in the inventory service, the Seata component will roll back the transaction operations of all participants to ensure data consistency and integrity.

By using Seata to manage distributed transactions, data consistency and integrity can be guaranteed in a distributed environment.
The code is as follows:
1. Add Seata dependency
First, add Seata in the order service, inventory service and payment service rely. For example, you can add the following to Maven's dependencies:

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>1.5.2</version>
</dependency>

2. Configure the Seata server
Before using Seata, you need to start the Seata server to provide distributed transaction coordination services. The Seata server needs to configure parameters such as storage mode and transaction log storage location. The server can be configured by modifying Seata's configuration file. For example, here is a simple Seata configuration file:

service {
    
    
  vgroup_mapping.mall_order_tx_group = "default"  # 订单服务所在的事务分组
  vgroup_mapping.mall_inventory_tx_group = "default"  # 库存服务所在的事务分组
  vgroup_mapping.mall_payment_tx_group = "default"  # 支付服务所在的事务分组
}

store {
    
    
  mode = "db"  # 存储模式,可以是 file、db、redis 等
  db {
    
    
    driver = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "seata"
    password = "seata"
    ...
  }
}

config {
    
    
  ...
}

3. Configure Seata client
In order service, inventory service and payment service, Seata client needs to be configured to connect to Seata server. The client can be configured by modifying Seata's configuration file. For example, here is a simple Seata configuration file:

service {
    
    
  vgroup_mapping.mall_order_tx_group = "default"  # 订单服务所在的事务分组
  vgroup_mapping.mall_inventory_tx_group = "default"  # 库存服务所在的事务分组
  vgroup_mapping.mall_payment_tx_group = "default"  # 支付服务所在的事务分组
}

client {
    
    
  ...
  rm {
    
    
    ...
  }
  tm {
    
    
    ...
  }
}

4. Code implementation
In the order service, inventory service and payment service, Seata's API needs to be used to manage distributed transactions. Here is a simple example code:

Order service:

@Service
public class OrderService {
    
    
    @Resource
    private OrderMapper orderMapper;
    @Resource
    private InventoryService inventoryService;
    @Resource
    private PaymentService paymentService;

    @GlobalTransactional(name = "mall_order_tx_group", rollbackFor = Exception.class)
    public void createOrder(Order order) {
    
    
        // 扣减库存
        inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
        // 创建订单
        orderMapper.insert(order);
        // 支付
        paymentService.pay(order.getUserId(), order.getAmount());
    }
}

Inventory service:

@Service
public class InventoryService {
    
    
    @Resource
    private InventoryMapper inventoryMapper;
    
	@GlobalLock
	@Transactional(rollbackFor = Exception.class)
	public void decreaseStock(Long productId, Integer quantity) {
    
    
	    // 扣减库存
	    inventoryMapper.decreaseStock(productId, quantity);
	}
}

Payment services:

@Service
public class PaymentService {
    
    
    @Transactional(rollbackFor = Exception.class)
    public void pay(Long userId, BigDecimal amount) {
    
    
        // 扣减余额
        ...
    }
}

In the order service, the entire business operation of creating an order is marked using the @GlobalTransactional annotation, which will enable Seata's global transaction. In this annotation, the transaction group name is specified as "mall_order_tx_group". In this method, the inventory service is first called to deduct the inventory, then the order is created, and finally the payment service is called to make the payment. Due to the @GlobalTransactional annotation, the entire operation is encapsulated in a distributed transaction.

In the inventory service, the @GlobalLock annotation is used to mark the business operation of deducting inventory, which turns on Seata's distributed lock. In this method, the @Transactional annotation is used to ensure the atomicity of database transactions. If an exception occurs during the inventory reduction process, Seata will prevent concurrency problems through a distributed lock mechanism.

In the payment service, the @Transactional annotation is used to ensure that the operation of deducting the balance has the atomicity of a database transaction.

Guess you like

Origin blog.csdn.net/qq_40454136/article/details/129517461