This is the fourth day of my participation in Gwen Challenge

This article is participating in “Java Theme Month – Java Development in Action”, see the activity link for details


In the previous article, we learned the construction of Seata and the use of AT mode. Through practice, we can find that in AT mode, users only need to pay attention to their own business, and the processing process of specific distributed transactions is transparent to users, which is suitable for the scenario where users do not want to transform their business. In addition to THE AT mode, there are TCC, Sage and XA modes in Seata. Next, we will continue to study the TCC mode and its usage process.

Different from AT mode, which does not require business transformation, TCC distributed transaction requires the developer to split the business logic, usually dividing the whole logic of the business system into three stages:

  • Try: Complete all service checks and reserve necessary service resources
  • Confirm: indicates the service logic to be executed. No service check is performed and only service resources reserved in the Try phase are used. Therefore, Confirm succeeds as long as the Try operation succeeds
  • Cancel: Releases service resources reserved during the Try phase. The Cancel operation must also be idempotent

According to the above description and comparison with AT mode, TCC mode has the following characteristics:

  1. TCC is the same as AT in that it is a two-phase commit, but TCC is very intrusive to the business code:
  • In AT mode, users only need to pay attention to their own business SQL, which is regarded as a phase. Seata framework will automatically generate two-phase commit and rollback operations of transactions
  • In TCC mode, all transactions manually implement the Try, Confirm, and Cancel methods
  1. TCC execution is more efficient
  • In AT mode, an attempt is made to obtain the global lock for the record before a local transaction commits
  • In TCC mode, data does not need to be locked globally and multiple transactions are allowed to operate data at the same time. Therefore, TCC is a solution for high-performance distributed transactions and is suitable for scenarios with high performance requirements

Next, take a look at how the TCC pattern needs to be applied in a specific business scenario. We carry on the transformation to the micro service in the last article, first modify the business logic of the order service. Create an order in 3 steps:

  • In the Try phase, the order is generated, but the order state is set to frozen, where 1 indicates the frozen state of the order and 0 indicates the normal state:

  • In Confirm stage, the transaction is submitted and the order is changed from frozen state to normal state:

  • Cancel phase, rollback transaction, delete order:

To use TCC mode, you need to create an interface:

@LocalTCC
public interface OrderTccAction {
    @TwoPhaseBusinessAction(name="orderAction",commitMethod = "commit",rollbackMethod = "rollback")
    boolean createOrder(BusinessActionContext businessActionContext,
                        @BusinessActionContextParameter(paramName = "order") Order order);
    boolean commit(BusinessActionContext businessActionContext);
    boolean rollback(BusinessActionContext businessActionContext);
}
Copy the code

On this interface, add the @localtCC annotation and declare three methods:

  1. Here,createOrderMethod corresponds to the try phase of the first phase
  • Method, specify two method names for the second phase through annotations
  • Parameters in a methodBusinessActionContextIs a context object used to pass data between two phases.
  • @BusinessActionContextParameterAnnotated parameter data is storedBusinessActionContext
  1. commitCommit action for phase 2
  2. rollbackIs the second phase rollback operation

In the implementation class, implement the business logic:

@Slf4j
@Component
public class OrderTccActionImpl implements OrderTccAction{

    @Autowired
    private OrderMapper orderMapper;

    @Override
    @Transactional
    public boolean createOrder(BusinessActionContext businessActionContext, Order order) {
        order.setStatus(1);
        orderMapper.insert(order);
        log.info("Create order: TCC phase 1 try successful");
        return true;
    }

    @Override
    @Transactional
    public boolean commit(BusinessActionContext businessActionContext) {
        JSONObject jsonObject= (JSONObject) businessActionContext.getActionContext("order");
        Order order=new Order();
        BeanUtil.copyProperties(jsonObject,order);
        order.setStatus(0);
        orderMapper.update(order,new LambdaQueryWrapper<Order>().eq(Order::getOrderNumber,order.getOrderNumber()));
        log.info("Create order: TCC phase 2 COMMIT succeeded");
        return true;
    }

    @Override
    @Transactional
    public boolean rollback(BusinessActionContext businessActionContext) {
        JSONObject jsonObject= (JSONObject) businessActionContext.getActionContext("order");
        Order order=new Order();
        BeanUtil.copyProperties(jsonObject,order);
        orderMapper.delete(new LambdaQueryWrapper<Order>().eq(Order::getOrderNumber,order.getOrderNumber()));
        log.info("Create order: TCC phase 2 rollback successful");
        return true; }}Copy the code

Modify the Service class:

@Service("orderTccService")
public class OrderTccServiceImpl implements OrderService{
    @Autowired
    OrderTccAction orderTccAction;

    @Override
    @GlobalTransactional
    public String buy(a){
        Order order=new Order();
        order.setOrderNumber(IdUtil.createSnowflake(1.1).nextIdStr())
                .setMoney(100D);
        boolean result = orderTccAction.createOrder(null, order);
        // if (result){
        // throw new RuntimeException(" exception test, prepare rollBack");
        // }       
        return "success"; }}Copy the code

The microservice was started and tested. First, the normal execution was tested. Both phases were successfully executed:

Throw the exception manually and you can see that the rollback is performed:

After testing a single microservice, the working condition of TCC distributed transaction under inter-microservice invocation is tested, and the inventory service is reformed. Similarly, split the inventory reduction operation, assuming the following data before operation on the inventory table:

  • In the Try stage, the amount of reserved deduction is taken out from the inventory quantity and frozen:

  • Confirm phase, commit transaction, use frozen inventory quantity to complete business data processing:

  • Cancel phase, roll back the transaction, unfreeze the frozen inventory, restore the previous inventory number:

When writing code, also create the interface first:

@LocalTCC
public interface StockTccAction {
    @TwoPhaseBusinessAction(name = "stockAction",commitMethod = "commit",rollbackMethod = "rollback")
    boolean reduceStock(BusinessActionContext businessActionContext,
                        @BusinessActionContextParameter(paramName = "proId") Long proId,
                        @BusinessActionContextParameter(paramName = "quantity") Integer quantity);
    boolean commit(BusinessActionContext businessActionContext);
    boolean rollback(BusinessActionContext businessActionContext);
}
Copy the code

Implementation class:

@Slf4j
@Component
public class StockTccActionImpl implements StockTccAction {
    @Autowired
    private StockMapper stockMapper;

    @Override
    @Transactional
    public boolean reduceStock(BusinessActionContext businessActionContext, Long proId, Integer quantity) {
        Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>().eq(Stock::getProId, proId));
        stock.setTotal(stock.getTotal()-quantity);
        stock.setFrozen(stock.getFrozen()+quantity);
        stockMapper.updateById(stock);
        log.info("Inventory reduction: TCC phase 1 try success");
        return true;
    }

    @Override
    @Transactional
    public boolean commit(BusinessActionContext businessActionContext) {
        long proId = Long.parseLong(businessActionContext.getActionContext("proId").toString());
        int quantity = Integer.parseInt(businessActionContext.getActionContext("quantity").toString());

        Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>().eq(Stock::getProId, proId));
        stock.setFrozen(stock.getFrozen()-quantity);
        stock.setSold(stock.getSold()+quantity);
        stockMapper.updateById(stock);

        log.info("Inventory reduction: TCC phase 2 Commit success");
        return true;
    }

    @Override
    @Transactional
    public boolean rollback(BusinessActionContext businessActionContext) {
        long proId = Long.parseLong(businessActionContext.getActionContext("proId").toString());
        int quantity = Integer.parseInt(businessActionContext.getActionContext("quantity").toString());

        Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>().eq(Stock::getProId, proId));
        stock.setTotal(stock.getTotal()+quantity);
        stock.setFrozen(stock.getFrozen()-quantity);
        stockMapper.updateById(stock);

        log.info("Inventory reduction: TCC Phase 2 rollback successful");
        return true; }}Copy the code

To test, call StockService in OrderService using FeigClient:

As you can see, the Tcc phase 2 of the inventory service has multiple COMMIT issues, which means that the interface can be called multiple times during phase 2, so we need to idempotent the interface. Add an idempotent handling utility class here to prevent methods from being initiated multiple times in the try phase and returning when the method is called again after a commit or ROLLBACK is successful. The HashBasedTable class from Guava is used here to simplify the case of determining a value by two keys, thus avoiding the nesting of maps.

public class IdempotentUtil {
    private staticTable<Class<? >,String,String> map=HashBasedTable.create();public static void addMarker(Class
        clazz,String xid,String marker){
        map.put(clazz,xid,marker);
    }

    public static String getMarker(Class
        clazz,String xid){
        return map.get(clazz,xid);
    }

    public static void removeMarker(Class
        clazz,String xid){ map.remove(clazz,xid); }}Copy the code

Using the Table data structure, we maintain a local cache of class and transaction xids as keys and tags as values. After the tag is deposited, it is checked to see if the tag exists during each commit or rollback phase. If the flag exists, it is the first time that the commit or rollback is performed. Perform the following business logic normally, and delete the flag when the execution is complete. If the tag does not exist, the execution is completed, and the subsequent business logic is not executed.

Modify the StockService by adding identifiers in the try phase, determining idempotent identifiers in all three phases, and removing them after commit or ROLLBACK:

@Override
@Transactional
public boolean reduceStock(BusinessActionContext businessActionContext, Long proId, Integer quantity) {
    if (Objects.nonNull(IdempotentUtil.getMarker(getClass(),businessActionContext.getXid()))){
        log.info("The try phase has been executed");
        return true;
    }
    // Business logic, omit...
    IdempotentUtil.addMarker(getClass(),businessActionContext.getXid(),"marker");
    return true;
}

@Override
@Transactional
public boolean commit(BusinessActionContext businessActionContext) {
    if (Objects.isNull(IdempotentUtil.getMarker(getClass(),businessActionContext.getXid()))){
        log.info("Commit phase has been performed");
        return true;
    }
   // Business logic, omit...
    log.info("Inventory reduction: TCC phase 2 Commit success");
    IdempotentUtil.removeMarker(getClass(),businessActionContext.getXid());
    return true;
}

@Override
@Transactional
public boolean rollback(BusinessActionContext businessActionContext) {
    if (Objects.isNull(IdempotentUtil.getMarker(getClass(),businessActionContext.getXid()))){
        log.info("Rollback phase has been implemented");
        return true;
    }
    // Business logic, omit...
    log.info("Inventory reduction: TCC Phase 2 rollback successful");
    IdempotentUtil.removeMarker(getClass(),businessActionContext.getXid());
    return true;
}
Copy the code

Execute again to view the result:

As you can see, the second COMMIT phase is skipped, ensuring that the business code is executed only once. Similarly, we manually throw an exception in the service to test for a local transaction failure:

As you can see, the rollback method is not executed a second time, avoiding repeated rollback situations. Idempotency is an important problem in THE TCC mode using Seata, because either the retransmission of network data or the compensation execution of abnormal transactions may lead to repeated execution of the Try, Confirm and Cancel phases. Only by checking idempotence can we ensure that the method guarantees the same business results no matter how many times it is repeated.

The last

If you think it is helpful, you can like it and forward it. Thank you very much

Wechat search: code agricultural ginseng, to add a friend, like the friend or ah ~

Public account background reply “interview”, “map”, “structure”, “actual combat”, get free information oh ~