Detailed explanation of seata's business non-invasive AT mode

Posted by rawisjp on Fri, 17 Dec 2021 03:30:27 +0100

Detailed explanation of non intrusive AT mode of seata service

GlobalTransactional annotation

1. Use this annotation to set the transaction name and timeout time. With this annotation, you will enter the GlobalTransactionalInterceptor in the aspect of Io seata. tm. api. TransactionalTemplate. Execute will assemble the transaction information according to the annotation and start the distributed global transaction

  // 2. begin transaction
            beginTransaction(txInfo, tx);

            Object rs = null;
            try {

                // Do Your Business
                rs = business.execute();

            } catch (Throwable ex) {

                // 3.the needed business exception to rollback.
                completeTransactionAfterThrowing(txInfo,tx,ex);
                throw ex;
            }

            // 4. everything is fine, commit.
            commitTransaction(tx);

Take a closer look at how to turn on global transactions

   public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
        throws TransactionException {
        GlobalBeginRequest request = new GlobalBeginRequest();
        request.setTransactionName(name);
        request.setTimeout(timeout);
        GlobalBeginResponse response = (GlobalBeginResponse)syncCall(request);
        return response.getXid();
    }

The implementation of the client is very simple. Send a synchronization message to the server
Let's look at the implementation of Io seata. server. coordinator. DefaultCoordinator#doGlobalBegin

     GlobalSession session = GlobalSession.
     
     createGlobalSession(
            applicationId, transactionServiceGroup, name, timeout);
        session.addSessionLifecycleListener(SessionHolder.getRootSessionManager());

        session.begin();
        return session.getXid();

Create distributed transactions and generate XID according to applicationId,transactionServiceGroup,name,timeout
Finally, put the generated GlobalSession into the sessionMap of GlobalSession and return XID to the client

After receiving the xid, the client will bind the xid to the thread and put it in threadLocal

2. How transactions are delivered to downstream services

Take Seata integrated Dubbo as an example io seata. integration. dubbo. alibaba. Transactionpropagationfilter inherits from Dubbo's filter

  if (xid != null) {
           RpcContext.getContext().setAttachment(RootContext.KEY_XID, xid);
       }

Put it into the RpcContext of dubbo, and it will be transmitted to the producer when calling the producer remotely.

3. How does rm join distributed transactions
The same is Io seata. integration. dubbo. alibaba. TransactionPropagationFilter

 if (rpcXid != null) {
                RootContext.bind(rpcXid);
                bind = true;
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("bind[" + rpcXid + "] to RootContext");
                }
            }

The xid of rpcContext will be placed in the RootContext of seata

When the rm transaction is committed,

public void commit() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Committing JDBC Connection [" + connection + "]");
      }
      connection.commit();
    }
  }

seata uses DataSourceProxy to return ConnectionProxy to wrap the real database connection when getConnection

 @Override
    public ConnectionProxy getConnection() throws SQLException {
        Connection targetConnection = targetDataSource.getConnection();
        return new ConnectionProxy(this, targetConnection);
    }

When the ConnectionProxy submits a transaction, it will register the branch transaction with the Seata server and record it in the ConnectionContext

  private void register() throws TransactionException {
        Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),
                null, context.getXid(), null, context.buildLockKeys());
        context.setBranchId(branchId);
    }

Take a look at the process of registering branch transactions with the seata server

 return globalSession.lockAndExcute(() -> {
            if (!globalSession.isActive()) {
                throw new TransactionException(GlobalTransactionNotActive, "Current Status: " + globalSession.getStatus());
            }
            if (globalSession.getStatus() != GlobalStatus.Begin) {
                throw new TransactionException(GlobalTransactionStatusInvalid,
                        globalSession.getStatus() + " while expecting " + GlobalStatus.Begin);
            }
            globalSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
            BranchSession branchSession = SessionHelper.newBranchByGlobal(globalSession, branchType, resourceId,
                    applicationData, lockKeys, clientId);
            if (!branchSession.lock()) {
                throw new TransactionException(LockKeyConflict);
            }
            try {
                globalSession.addBranch(branchSession);
            } catch (RuntimeException ex) {
                throw new TransactionException(FailedToAddBranch);
            }
            return branchSession.getBranchId();
        });

The global session is locked for operation. The branch transaction session is put into the branchSessions of the global transaction session and the branch transaction id is returned. And in io seata. server. lock. memory. Memorylocker#acquireliock attempts to add a global lock. If locking fails, the branch transaction commit fails.

seata uses spi mechanism to generate locks. dubbo uses spi mechanism in order to flexibly switch the registry and the protocol used

The role of spi is very similar to the strategist pattern in the design pattern. There are many different implementations under one interface. When using, you only need to specify name.

Implement the key class EnhancedServiceLoader, taking the spi of the registry as an example,

EnhancedServiceLoader.load(RegistryProvider.class, Objects.requireNonNull(registryType).name()).provide();

EnhancedServiceLoader will use ClassLoader to load into the jvm from the specified path according to RegistryProvider, obtain the class object, and then instantiate the returned object

Using spi mechanism, the plug-in between modules can be easily realized, and different implementations can be used without modifying the code

To sum up, the policy mode and SPI mechanism have the following similarities and differences:

From the design idea. The idea of policy mode and SPI mechanism is similar. They both isolate the changed parts through certain design, so as to make the original parts more stable.
In terms of isolation level. Policy mode isolation is class level isolation, while SPI mechanism is project level isolation.
From the perspective of application field. Policy mode is more used in business code writing, and SPI mechanism is more used in framework design.

seata uses its approval mechanism to create a memoryLock to try to add a global lock. If the current lockkey has no lock, put the lockkey in, and the locking succeeds. Otherwise, it fails and returns a conflict error

seata rollback

How is global rollback triggered?
1. Branch transaction abnormal rollback causes global rollback
2. Timeout causes rollback io seata. server. coordinator. Defaultcoordinator #timeoutcheck detects whether the transaction timed out

Abnormal rollback analysis, look back to io seata. spring. annotation. GlobalTransactionalInterceptor

 try {

                // Do Your Business
                rs = business.execute();

            } catch (Throwable ex) {

                // 3.the needed business exception to rollback.
                completeTransactionAfterThrowing(txInfo,tx,ex);
                throw ex;
            }

An exception triggered a rollback

  @Override
    public GlobalStatus rollback(String xid) throws TransactionException {
        GlobalRollbackRequest globalRollback = new GlobalRollbackRequest();
        globalRollback.setXid(xid);
        GlobalRollbackResponse response = (GlobalRollbackResponse)syncCall(globalRollback);
        return response.getGlobalStatus();
    }

Send a rollback request to Seata server

Seata server is in io seata. server. coordinator. Defaultcore#rollback processing request

  boolean shouldRollBack = globalSession.lockAndExcute(() -> {
            globalSession.close(); // Highlight: Firstly, close the session, then no more branch can be registered.
            if (globalSession.getStatus() == GlobalStatus.Begin) {
                globalSession.changeStatus(GlobalStatus.Rollbacking);
                return true;
            }
            return false;
        });

         doGlobalRollback(globalSession, false);

Change the status of the global session to globalstatus Rollbacking start global rollback
The server traverses all branch sessions, then sends synchronization and waits for branch transaction rollback to succeed

    BranchRollbackResponse response = (BranchRollbackResponse)messageSender.sendSyncRequest(resourceId,
                branchSession.getClientId(), request);

Client rollback: io seata. rm. datasource. Datasourcemanager #branchrollback, perform undo operation

            UndoLogManager.undo(dataSourceProxy, xid, 

First, query undo log according to branchid and xid. That is, check undo for branch id and global transaction id_ Log table, undo_ The beforeImage and afterImage are recorded in the log table, that is, the data before and after the branch transaction is executed. The data will be verified before rolling seata. rm. datasource. undo. AbstractUndoExecutor#dataValidationAndGoOn
beforeImage is used to rollback the recovered data. Afterimage is used to determine whether the data has been dirty. Before undo, you need to check whether afterimage and currentRecord are equal

        if (!DataCompareUtils.isRecordsEquals(afterRecords, currentRecords)) {
            XXXXXXX
        }

Pay attention to io seata. rm. datasource. DataCompareUtils#isFieldEquals,

  return f0.getValue().equals(f1.getValue());

Judged by equals, if the floating-point number is set in the database, it will always be judged that it is not equal to, so it cannot be rolled back

After the judgment is completed, the update statement will be executed to update the data to beforeImage, and the branch transaction rollback is completed

response set branchid and xid status to phasewo_ The rollbacked is returned to the server

Back to Seata server
The server receives phasewo_ In response to rollbacked, remove branchSession

 case PhaseTwo_Rollbacked:
                        globalSession.removeBranch(branchSession);
                        LOGGER.error("Successfully rolled back branch " + branchSession);
                        continue;

Let's look at the rollback failure

 try {
            UndoLogManager.undo(dataSourceProxy, xid, branchId);
        } catch (TransactionException te) {
            if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {
                return BranchStatus.PhaseTwo_RollbackFailed_Unretryable;
            } else {
                return BranchStatus.PhaseTwo_RollbackFailed_Retryable;
            }
        }

PhaseTwo_RollbackFailed_Unretryable failed to rollback. Do not attempt to rollback again
PhaseTwo_RollbackFailed_Retryable failed to rollback, attempting to rollback

catch (Throwable e) {
                if (conn != null) {
                    try {
                        conn.rollback();
                    } catch (SQLException rollbackEx) {
                        LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx);
                    }
                }
                throw new TransactionException(BranchRollbackFailed_Retriable, String.format("%s/%s", branchId, xid),
                    e);

            } 

It can be seen from the code that unless there is an error, the exceptions under Throwable will return the rollback failure of the attempt to rollback. After the array dirty, the transaction will fail to rollback and will continue to retry until the timeout time is reached. Configured as max.rollback retry. timeout

Server code

     switch (branchStatus) {
                    case PhaseTwo_Rollbacked:
                        globalSession.removeBranch(branchSession);
                        LOGGER.error("Successfully rolled back branch " + branchSession);
                        continue;
                    case PhaseTwo_RollbackFailed_Unretryable:
                        SessionHelper.endRollbackFailed(globalSession);
                        LOGGER.error("Failed to rollback global[" + globalSession.getXid() + "] since branch["
                            + branchSession.getBranchId() + "] rollback failed");
                        return;
                    default:
                        LOGGER.info("Failed to rollback branch " + branchSession);
                        if (!retrying) {
                            queueToRetryRollback(globalSession);
                        }
                        return;

                }

Except PhaseTwo_RollbackFailed_Unretryable, the status changes to rollback failure, and there is no return. Other failures will be put into the retry queue, and the rollback attempt will be made again, and the session status changes to retry

    retryRollbacking.scheduleAtFixedRate(() -> {
            try {
                handleRetryRollbacking();
            } catch (Exception e) {
                LOGGER.info("Exception retry rollbacking ... ", e);
            }
        }, 0, rollbackingRetryDelay, TimeUnit.SECONDS);

The default is 5 seconds. All sessions are taken from the rollback retry queue each time to try to rollback one by one. If the rollback time exceeds the maximum time, the rollback will also be abandoned

 Collection<GlobalSession> rollbackingSessions = SessionHolder.getRetryRollbackingSessionManager().allSessions();
        if(CollectionUtils.isEmpty(rollbackingSessions)){
            return;
        }
        long now = System.currentTimeMillis();
        for (GlobalSession rollbackingSession : rollbackingSessions) {
            try {
                if(isRetryTimeout(now, MAX_ROLLBACK_RETRY_TIMEOUT.toMillis(), rollbackingSession.getBeginTime())){
                    /**
                     * Prevent thread safety issues
                     */
                    SessionHolder.getRetryCommittingSessionManager().removeGlobalSession(rollbackingSession);
                    LOGGER.error("GlobalSession rollback retry timeout [{}]", rollbackingSession.getTransactionId());
                    continue;
                }
                rollbackingSession.addSessionLifecycleListener(SessionHolder.getRootSessionManager());
                core.doGlobalRollback(rollbackingSession, true);
            } catch (TransactionException ex) {
                LOGGER.info("Failed to retry rollbacking [{}] {} {}",
                    rollbackingSession.getXid(), ex.getCode(), ex.getMessage());
            }
        }

Detect transaction timeout
Scheduled task io seata. server. coordinator. Defaultcoordinator #timeoutcheck, which is detected every 5 seconds by default

 public boolean isTimeout() {
        return (System.currentTimeMillis() - beginTime) > timeout;
    }

Submitted by seata

During the whole transaction execution process, there is no exception and no timeout, and the transaction will be submitted normally

1. Who triggers the submit operation?
The transaction initiator will commit the transaction if there is no exception
When the client submits a transaction, it sends a transaction submission request to Seata server
The server handles the transaction submission request io seata. server. coordinator. Defaultcore #doglobalcommit traverses all branch transactions of the global transaction, commits them separately, and also sends a synchronization message.

2. Client submission process

Because the local transaction of the branch transaction has been committed after execution, there is no operation when the branch transaction is finally committed

   if (!ASYNC_COMMIT_BUFFER.offer(new Phase2Context(branchType, xid, branchId, resourceId, applicationData))) {
            LOGGER.warn("Async commit buffer is FULL. Rejected branch [" + branchId + "/" + xid + "] will be handled by housekeeping later.");
        }
        return BranchStatus.PhaseTwo_Committed;

The second paragraph has been submitted,
After all branch transactions are committed successfully, the global session ends

3 after all submissions are completed, the transaction initiator clears xid and other data, and the distributed transaction is completed

       // 4. everything is fine, commit.
            commitTransaction(tx);

            return rs;
        } finally {
            //5. clear
            triggerAfterCompletion();
            cleanUp();
        }

Topics: Java Database