TCC Demo code implementation


Introduction to the

Design and implement a TCC distributed transaction framework simple Demo, transaction manager, do not need to achieve global transaction persistence and recovery, high availability and so on

Project running

  • Project address: TCCDemo

The MySQL database is required to store global transaction information and the relevant TCC steps are printed on the console

  • 1: Start MySQL and create a database test
  • 2. Run the TccDemoApplication of the current project, and automatically create the table of the database after starting
  • 3. Visit: http://localhost:8080/transaction/commit, and confirm the sample
  • 4. Visit: http://localhost:8080/transaction/cancel, cancel the sample

General implementation idea

  • 1. Initialization: Register a new transaction with the transaction manager and generate a global transaction unique ID
  • 2. Try phase execution: during the execution of the try-related code, the corresponding call record is registered, and the result of the try execution is sent to the transaction manager. Upon successful execution, the transaction manager performs confirm or Cancel steps
  • 3. Confirm phase: After receiving the message that the try execution succeeds, the transaction manager enters the confirm phase based on the transaction ID. If the confirm fails, the transaction manager enters the Cancel phase
  • 4. Cancel phase: When the transaction manager receives a try execution failure or confirm execution failure, the transaction manager enters the cancel phase according to the transaction ID. If the execution failure occurs, the transaction manager prints logs or alarms and asks manual workers to handle the failure

Front knowledge

Principle of TCC

There are three main phases of TCC distributed transactions:

  • 1.Try: detects and reserves resources for service systems
  • 2.Confirm: Confirm the operation
  • 3.Cancel: Cancels the service operation

Here is an example of what needs to be done in the three phases: for example, there are now two databases, one user account database and one commodity inventory database, which now provides an interface to buy goods. When the purchase is successful, the user account and the commodity inventory are deducted, roughly the pseudo-code is as follows:

public void buy(a) {
    // User account operations
    userAccount();
    // Merchandise account operation
    StoreAccount();
}
Copy the code

In the above operation, both functions must succeed at the same time, otherwise data inconsistency will occur, that is, transaction atomicity needs to be guaranteed.

Because the setting scenario is data in two different databases, so there is no way to utilize the transaction mechanism of a single database, it is cross-database, so the mechanism of distributed transaction is needed.

Below is a simple simulation, without using TCC transaction manager, according to TCC idea, how to guarantee transaction atomicity in code

TCC no transaction manager Demo pseudocode

Using the above scenario, the code looks like this:

class Demo {
    
    public void buy(a) {
        // Try stage: for example, to judge whether the balance and deposit of users and goods are sufficient, and to deduct money and reduce inventory
        if(! userServer.tryDeductAccount()) {// The user failed to withhold money, the relevant data has not changed, just return an error
        }
        if(! storeService.tryDeductAccount()) {// Cancel stage: pre-destocking of goods failed, because the user's pre-payment was made before, so we need to enter the Cancel stage to restore the user account
            userService.cancelDeductAccount();
        }

        // Confirm stage: Try the Confirm stage after the success. This part of the operation, for example, set the successful deduction state and inventory reduction state to complete
        if(! userService.confirmDeductAccount() || ! storeService.confirmDeductAccount()) {// Cancel phase: Any phase of confirm fails and data recovery (rollback) is requireduserService.cancelDeductAccount(); storeService.cancelDeductAccount(); }}}Copy the code

Each previous function operation needs to be divided into three subfunctions: try, Confirm, and cancel. Refine it, judge the execution in the code, and keep it atomically transactional.

Above are two services, user account and goods store operation. It doesn’t seem like too much to write, but what if there are multiple services? In the try phase, there will be a lot more if, and the corresponding dynamic increase of cancel, as well as confirm, which is roughly as follows:

class Demo {
    
    public void buy(a) {
        // Try stage: for example, to judge whether the balance and deposit of users and goods are sufficient, and to deduct money and reduce inventory
        if(! userServer.tryDeductAccount()) {// The user failed to withhold money, the relevant data has not changed, just return an error
        }
        if(! storeService.tryDeductAccount()) {// Cancel stage: pre-destocking of goods failed, because the user's pre-payment was made before, so we need to enter the Cancel stage to restore the user account
            userService.cancelDeductAccount();
        }
        // Try is added, cancel is added dynamically
        if(! xxxService.tryDeductAccount()) { xxxService.cancelDeductAccount(); xxxService.cancelDeductAccount(); }if(! xxxService.tryDeductAccount()) { xxxService.cancelDeductAccount(); xxxService.cancelDeductAccount(); xxxService.cancelDeductAccount(); }...// Confirm stage: Try the Confirm stage after the success. This part of the operation, for example, set the successful deduction state and inventory reduction state to complete
        if(! userService.confirmDeductAccount() || ! storeService.confirmDeductAccount() || ......) {// Cancel phase: Any phase of confirm fails and data recovery (rollback) is requireduserService.cancelDeductAccount(); storeService.cancelDeductAccount(); . }}}Copy the code

It can be seen that the code similarity is a lot, the project similar need distributed call a lot, so that a large number of such similar code will be flooded in the project, in order to be lazy, the introduction of TCC transaction manager can simplify a lot

TCC transaction manager

In order to be lazy, use a transaction manager, but what part of laziness is stolen? In the previous code, the try phase was delegated to the local program, while confirm and Cancel were delegated to the transaction manager. Here is the TCC pseudocode for Seata and Hmily:

interface UserService {

    @TCCAction(name = "userAccount", confirmMethod = "confirm", cancelMethod = "cancel")
    public void try(a);

    public void confirm(a);

    public void cancel(a);
}

interface StoreService {

    @TCCAction(name = "userAccount", confirmMethod = "confirm", cancelMethod = "cancel")
    public void try(a);

    public void confirm(a);

    public void cancel(a);
}

class Demo {

    @TCCGlobalTransaction
    public String buy(a) {
        if(! userService.buy()) {throw error;
        }
        if(! storeService.try()) {
            throw error;
        }
        returnTcc.xid(); }}Copy the code

Debugging reference Seata and Hmily TCC, figure out the general steps, which have a lot of details, ignore for the moment, mainly look at the general implementation

Here is a lot of simplification, using even a comment, can roughly complete the entire TCC process, the following is the entire TCC Demo running process:

  • 1. Initialization: Register a new transaction with the transaction manager and generate a global transaction unique ID
  • 2. Try phase execution: during the execution of the try-related code, the corresponding call record is registered, and the result of the try execution is sent to the transaction manager. Upon successful execution, the transaction manager performs confirm or Cancel steps
  • 3. Confirm phase: After receiving the message that the try execution succeeds, the transaction manager enters the confirm phase based on the transaction ID. If the confirm fails, the transaction manager enters the Cancel phase
  • 4. Cancel phase: When the transaction manager receives a try execution failure or confirm execution failure, the transaction manager enters the cancel phase according to the transaction ID. If the execution failure occurs, the transaction manager prints logs or alarms and asks manual workers to handle the failure
1. Initialization

This step is basically to register to generate a new global transaction and get the unique identifying ID of the new transaction.

@tCCGlobalTransaction, this annotation is used to mark the start of a transaction, register a new transaction, place the ID of the new transaction into the current threadLocal, and subsequent function implementations can retrieve the current transaction ID to perform their own operations

2. Try stage

This phase is mainly the execution of the try operation of each called service, such as userservice.try ()/ storeservice.try ().

Add the @tccAction annotation to register the transaction’s function call record: Because the number of transactions is uncertain, when this annotation is added, the interception calls will register the confirm and Cancel methods of the subtransaction with the transaction manager based on the transaction ID obtained from threadLocal, so that the later transaction manager can register the confirm and cancel methods of the subtransaction based on the transaction ID. Facilitate the execution of relevant confirmations and Cancels

3. Confirm stage:

When the buy () function above completes and succeeds, a message is sent to the transaction manager, which uses the transaction ID to facilitate the subsequent confirm phase

4. Cancel phase:

When buy fails or confirm fails, the transaction manager pushes execution into the Cancel phase based on the current transaction ID

Code implementation

The code implementation section only lists the key code, the code is a little bit too much, the code implementation logic line is as follows:

  • 1. Intercept @tCCGlobalTransaction: generate the global transaction ID
  • 2. Intercept @tCCAction during the execution of the transaction function: register branch transaction call information in the global transaction management database
  • 3. When the try phase succeeds or fails, send a message to the global transaction manager
  • 4. The global transaction manager says the end of execution trigger for the try node and sends a message to push the confirm or Cancel phases of each branch transaction

1. Intercept @tCCGlobalTransaction: generate the global transaction ID

Define the associated @tCCGlobalTransaction annotation as follows:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TccTransaction {
}
Copy the code

The example global transaction uses the following:

@Slf4j
@Component
public class TransactionService {

    @Autowired
    private UserAccountServiceImpl user;

    @Autowired
    private StoreAccountServiceImpl store;

    @TccTransaction
    public void buySuccess(a) {
        log.info("global transaction id:: " + RootContext.get());
        if(! user.prepare(true)) {
            log.info("user try failed");
            throw new RuntimeException("user prepare failed!");
        }
        log.info("user try success");
        if(! store.prepare(true)) {
            log.info("store try failed");
            throw new RuntimeException("store prepare failed");
        }
        log.info("store try success"); }}Copy the code

Intercepts the annotation, generates the global transaction ID, puts it into threadLocal, and later functions retrieve the ID, roughly as follows:

/** * global transaction {@TccTransacton} Interception processing * is used to generate the global transaction unique identifier ID, which is registered to the transaction manager for generation *@author lw
 */
@Aspect
@Component
@Slf4j
public class GlobalTransactionHandler {

    private final TransactionInfoMapper transactionInfoMapper;

    public GlobalTransactionHandler(TransactionInfoMapper transactionInfoMapper) {
        this.transactionInfoMapper = transactionInfoMapper;
    }

    @Pointcut("@annotation(com.tcc.demo.demo.annotation.TccTransaction)")
    public void globalTransaction(a) {}

    /** * intercepts global transactions *@param point
     * @return
     * @throws UnknownHostException
     */
    @Around("globalTransaction()")
    public Object globalTransactionHandler(ProceedingJoinPoint point) throws UnknownHostException {
        log.info("Global transaction handler");

        // Generate a global transaction ID and place it in a threadLocalString transactionId = createTransactionId(); RootContext.set(transactionId); .return null;
    }

    /** * Generate global transaction ID: local IP address + local branch transaction manager listening port + timestamp *@return xid
     * @throws UnknownHostException UnknownHostException
     */
    private String createTransactionId(a) throws UnknownHostException {
        String localAddress = InetAddress.getLocalHost().getHostAddress();
        String timeStamp = String.valueOf(System.currentTimeMillis());
        return localAddress + ": 8080:"+ timeStamp; }}Copy the code

2. Intercept @tCCAction during the execution of the transaction function: register branch transaction call information in the global transaction management database

The definition of an annotation is roughly as follows:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TccAction {

    String name(a);

    String confirmMethod(a);

    String cancelMethod(a);
}
Copy the code

An example is as follows:

@Component
@Slf4j
public class StoreAccountServiceImpl implements Service {

    @Override
    @TccAction(name = "prepare", confirmMethod = "commit", cancelMethod = "cancel")
    public boolean prepare(boolean success) {... }@Override
    public boolean commit(a) {... }@Override
    public boolean cancel(a) {... }}Copy the code

When a branch transaction executes a try (prepare), it needs to be intercepted and registered with global transaction management. The code is as follows:

@Aspect
@Component
@Slf4j
public class BranchTransactionHandler {

    private final TccClientService tccClientService;

    public BranchTransactionHandler(TccClientService tccClientService) {
        this.tccClientService = tccClientService;
    }

    @Pointcut(value = "@annotation(com.tcc.demo.demo.annotation.TccAction)")
    public void branchTransaction(a) {}

    @Before("branchTransaction()")
    public void branchTransactionHandler(JoinPoint point) throws Throwable {
        log.info("Branch transaction handler :: " + RootContext.get());

        // Get the branch transaction service class name for later reflection class loading
        Object target = point.getTarget().getClass();
        String className = ((Class) target).getName();

        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        TccAction tccActionAnnotation = method.getAnnotation(TccAction.class);

        // Get the corresponding method names for confirm and cancel
        String commitMethodName = tccActionAnnotation.confirmMethod();
        String cancelMethodName = tccActionAnnotation.cancelMethod();

        // Write to global transaction management datatccClientService.register(RootContext.get(), className, commitMethodName, cancelMethodName); }}Copy the code

3. When the try phase succeeds or fails, send a message to the global transaction manager

In @tCCTransaction, the entire execution of the function is called, which triggers the execution of each branch transaction @tCCAction, which is the try phase. When a try fails or succeeds, global transaction management pushes into the Confirm or Cancel phase. The general code is as follows:

/** * global transaction {@TccTransacton} Interception processing * is used to generate the global transaction unique identifier ID, which is registered to the transaction manager for generation *@author lw
 */
@Aspect
@Component
@Slf4j
public class GlobalTransactionHandler {

    private final TransactionInfoMapper transactionInfoMapper;

    public GlobalTransactionHandler(TransactionInfoMapper transactionInfoMapper) {
        this.transactionInfoMapper = transactionInfoMapper;
    }

    @Pointcut("@annotation(com.tcc.demo.demo.annotation.TccTransaction)")
    public void globalTransaction(a) {}

    /** * intercepts global transactions *@param point
     * @return
     * @throws UnknownHostException
     */
    @Around("globalTransaction()")
    public Object globalTransactionHandler(ProceedingJoinPoint point) throws UnknownHostException {
        log.info("Global transaction handler");

        // Generate a global transaction ID and place it in a threadLocal
        String transactionId = createTransactionId();
        RootContext.set(transactionId);

        try {
            // Execution of the try phase
            point.proceed();
        } catch (Throwable throwable) {
            // Update the status of all branch transactions in the database after a try failure
            log.info("global update transaction status to try failed");
            updateTransactionStatus(transactionId, TransactionStatus.TRY_FAILED);
            log.info("global update transaction status to try failed end");

            // Send a message to push into the Cancel phase
            log.info(transactionId + " global transaction try failed, will rollback");
            sendTryMessage(transactionId);
            return null;
        }

        // Try succeeds, updating the status of all branch transactions in the database
        log.info("global update transaction status to try success");
        updateTransactionStatus(transactionId, TransactionStatus.TRY_SUCCESS);
        log.info("global update transaction status to try success end");

        // Send the message to enter the confirm phase. If the confirm fails, send the message again to enter the Cancel phase
        log.info(transactionId + " global transaction try success, will confirm");
        if(! sendTryMessage(transactionId)) { log.info(transactionId +" global transaction confirm failed, will cancel");
            sendTryMessage(transactionId);
        }

        return null;
    }

    /** * Sends the message to the branch transaction manager (TM) * when TM receives the message, it queries the transaction database and determines whether to perform confirm or cancel based on the transaction status@param transactionId xid
     * @return execute result
     */
    private boolean sendTryMessage(String transactionId) {
        log.info("send message to local TM to execute next step");
        String[] slice = transactionId.split(":");
        String targetHost = slice[0];
        String targetPort = slice[1];

        RestTemplate restTemplate = new RestTemplate();
        String url = "http://" + targetHost + ":" + targetPort + "/tm/tryNext? xid=" + transactionId;
        Boolean response = restTemplate.getForObject(url, boolean.class, new HashMap<>(0));

        if (response == null| |! response) { log.info("try next step execute failed, please manual check");
            return false;
        } else {
            log.info("try next step execute success");
            return true; }}/** * Generate global transaction ID: local IP address + local branch transaction manager listening port + timestamp *@return xid
     * @throws UnknownHostException UnknownHostException
     */
    private String createTransactionId(a) throws UnknownHostException {
        String localAddress = InetAddress.getLocalHost().getHostAddress();
        String timeStamp = String.valueOf(System.currentTimeMillis());
        return localAddress + ": 8080:" + timeStamp;
    }

    /** * Update the execution status of all branch transactions by xID *@param xid xid
     * @param status status
     */
    private void updateTransactionStatus(String xid, int status) {
        TransactionInfo transactionInfo = new TransactionInfo();
        transactionInfo.setXid(xid);
        transactionInfo.setStatus(status);
        try {
            transactionInfoMapper.updateOne(transactionInfo);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

4. The global transaction manager says the end of execution trigger for the try node and sends a message to push the confirm or Cancel phases of each branch transaction

The branch transaction manager (TM) receives the message, pulls out all the branch transactions based on the XID, and determines whether to perform confirm or Cancel based on the status

If the branch transaction is registered, the branch transaction must be registered. If the branch transaction is registered, the branch transaction must be registered. If the try does not execute, it must be the branch transaction that failed, just restore the previous data.

The general code is as follows:

@Service
@Slf4j
public class TccClientService {

    private final TransactionInfoMapper transactionInfoMapper;

    public TccClientService(TransactionInfoMapper transactionInfoMapper) {
        this.transactionInfoMapper = transactionInfoMapper;
    }

    If a branch transaction fails, cancel will be performed. If all branch transactions succeed, confirm will be performed@param xid xid
     * @returnReturn confirm or Cancel */
    public boolean transactionHandle(String xid) {
        // Query all branch transaction information according to xID
        Map<String, Object> condition = new HashMap<>(1);
        condition.put("xid", xid);
        List<Map<String, Object>> branchTransactions = transactionInfoMapper.query(condition);

        // Determine whether all transaction tries were successfully executed, confirm, or cancel
        boolean executeConfirm = true;
        for (Map<String, Object> item: branchTransactions) {
            if (item.get("status").equals(TransactionStatus.TRY_FAILED) || item.get("status").equals(TransactionStatus.CONFIRM_FAILED)) {
                executeConfirm = false;
                break; }}// Perform confirm or cancel
        if (executeConfirm) {
            return executeMethod(branchTransactions, TransactionMethod.CONFIRM);
        } else {
            returnexecuteMethod(branchTransactions, TransactionMethod.CANCEL); }}/** * The branch transaction registers the class name and method name, and the corresponding confirm or cancel method is called@paramBranchTransactions Branch transaction information *@paramMethodName Confirm or Cancel *@return bool
     */
    private boolean executeMethod(List<Map<String, Object>> branchTransactions, String methodName) {
        for (Map<String, Object> item: branchTransactions) {
            log.info("service info:: " + item.toString());
            log.info("service method :: " + item.get(methodName).toString());

            try{ Class<? > clazz = Class.forName(item.get("class_name").toString());
                log.info("Service Class::" + clazz.getName());

                Method method = clazz.getDeclaredMethod(item.get(methodName).toString());
                log.info("Service Method::" + method.toString());

                Object service = clazz.newInstance();
                Object ret = method.invoke(service);
                log.info("execute method return: " + ret.toString());
            } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
                e.printStackTrace();
                return false; }}return true; }}Copy the code

conclusion

At this point, a very rudimentary TCC Demo has been implemented. The roles of TC and TM are not particularly clear, because they are basically embedded in an application, but still reflect the general idea. Of course, TCS are completely separable, like Seata, which is a separate Server.

TC and TM communication methods can also be used in other ways, such as RPC for the convenience of HTTP.

The complete project is as follows: TCCDemo