Performance stress testing

更新时间:
复制 MD 格式

This topic simulates a fund transfer scenario and implements the transfer feature using the TCC, SAGA, and FMT patterns for distributed transactions. It also provides a general overview of the performance of these patterns based on stress testing results.

Stress testing model

As shown in the following figure, Application A exposes a transfer service. The service transfers funds from Account A to Account B.

model

The implementation flow is as follows:

  1. The transfer service starts a distributed transaction.

  2. Participant A, which is provided by Application A (the initiator), debits funds from Account A.

  3. Participant B, which is provided by Application B, credits funds to Account B.

Note

The account data for Account A is stored in Database A and is accessed only by Application A. The account data for Account B is stored in Database B and is accessed by Application B.

Implement the transfer

Table schema design

The schema of table A in the database is as follows:

## Account balance table, which stores account balance information
create table account(
    account_no varchar(256)notnull comment 'Account',
    amount DOUBLE comment 'Account balance',
    freezed_amount DOUBLE comment 'Frozen amount. This field is used only in the TCC pattern.',
    primary key (account_no)
);

## Account transaction log table, which records the account, amount, and operation type (debit/credit) for each distributed transaction operation. This table is used only in the TCC pattern.
create table account_transaction(
    tx_id varchar(256)notnull,
    account_no varchar(256)notnull,
    amount DOUBLE,
    type varchar(256)notnull,
    primary key (tx_id)
);

The schema of table B is the same as that of table A.

Implementing the TCC pattern

Initiator implementation:

 @DtxTransaction(bizType = "single-transfer-by-tcc")
    public boolean transferByTcc(String from, String to, double amount) {
        try {
            // First participant
            boolean ret = firstTccActionRef.prepare_minus(null, from, amount);
            if (!ret) {
                // Transaction rollback
                RuntimeContext.setRollBack();
                return false;
            }
            // Second participant
            ret = secondTccActionRef.prepare_add(null, to, amount);
            if (!ret) {
                // Transaction rollback
                RuntimeContext.setRollBack();
                return false;
            }
            return ret;
        } catch (Throwable t) {
            throw new RuntimeException(t);
        }
    }

Participant A (debit) implementation:

public interface FirstTccAction {

    @TwoPhaseBusinessAction(name = "firstTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare_minus(BusinessActionContext businessActionContext, String accountNo,
                                 double amount);

    public boolean commit(BusinessActionContext businessActionContext);

    public boolean rollback(BusinessActionContext businessActionContext);
}

Implementation for Participant B (Payer):

public interface SecondTccAction {

    @TwoPhaseBusinessAction(name = "secondTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare_add(BusinessActionContext businessActionContext, String accountNo, double amount);

    public boolean commit(BusinessActionContext businessActionContext);

    public boolean rollback(BusinessActionContext businessActionContext);
}

Implementing the FMT pattern

Initiator implementation:

    @DtxTransaction(bizType = "transfer-by-auto")
    public boolean transferByAuto(String from, String to, double amount) {
        boolean ret = false;
        try {
            // First participant
            ret = firstAutoAction.amount_minus(from, amount);
            if (!ret) {
                // Transaction rollback
                RuntimeContext.setRollBack();
                return false;
            }
            // Second participant
            ret = secondAutoAction.amount_add(to, amount);
            if (!ret) {
                // Transaction rollback
                RuntimeContext.setRollBack();
                return false;
            }
            return ret;
        } catch (Throwable t) {
            throw new RuntimeException(t);
        }
    }

Implementation of Participant A (debit):

public class FirstAutoActionImpl implements FirstAutoAction {

......

    @Override
    public boolean amount_minus(final String accountNo, final double amount) {
        try {
            return tccFirstActionTransactionTemplateAuto.execute(new TransactionCallback<Boolean>() {
                @Override
                public Boolean doInTransaction(TransactionStatus status) {
                    try {
                        Account account = firstAccountDAOAuto.getAccountForUpdate(accountNo);
                        if (account == null) {
                            throw new RuntimeException("Account does not exist");
                        }
                        // Debit
                        double newAmount = account.getAmount() - amount;
                        if (amount < 0) {
                            throw new RuntimeException("Insufficient balance");
                        }
                        account.setAmount(newAmount);
                        int n = firstAccountDAOAuto.updateAmount(account);
                        if (n == 1) {
                            return true;
                        } else {
                            status.setRollbackOnly();
                            return false;
                        }
                    } catch (Exception e) {
                        logger.error("amount_minus error", e);
                        status.setRollbackOnly();
                        return false;
                    }
                }
            });
        } catch (Exception e) {
            logger.error("amount_minus failed", e);
            return false;
        }
    }
}

Implementing Participant B (Payer):

public class SecodeAutoActionImpl implements SecondAutoAction {

......

    @Override
    public boolean amount_add(final String accountNo, final double amount) {
        try {
            return tccSecondActionTransactionTemplateAuto.execute(new TransactionCallback < Boolean > () {
                @Override
                public Boolean doInTransaction (TransactionStatus status){
                    try {
                        Account account = secondAccountDAOAuto.getAccountForUpdate(accountNo);
                        if (account == null) {
                            throw new RuntimeException("Account does not exist");
                        }
                        // Credit
                        double newAmount = account.getAmount() + amount;
                        account.setAmount(newAmount);
                        secondAccountDAOAuto.updateAmount(account);
                    } catch (Exception e) {
                        logger.error("amount_add error", e);
                        status.setRollbackOnly();
                        return false;
                    }
                    return true;
                }
            });
        } catch (Exception e) {
            logger.error("amount_add failed", e);
            return false;
        }
    }
}

Implement the SAGA pattern

You can use a SAGA state machine to design the business flow, as shown in the following figure:

  • amountMinus is the debit service.

  • amountAdd is the service used to add money.

  • compensateMinus is the compensation (reversal) service that cancels a debit transaction.

  • compensateAdd is the compensation service that cancels a credit transaction.

State machine

Initiator implementation:

public class TransferServiceImpl implements TransferService {
    protected final static Logger logger = LoggerFactory.getLogger(TransferServiceImpl.class);
    @Autowired
    private StateMachineEngine stateMachineEngine;

    @Override
    public boolean transferBySaga(String from, String to, BigDecimal amount) {
        try {
            // Generate a business ID
            String businessKey = UUID.randomUUID().toString().replaceAll("-", "");

            Map<String, Object> params = new HashMap<>(4);
            params.put("from", from); // Transfer-out account
            params.put("to", to); // Transfer-in account
            params.put("amount", amount); // Transfer amount
            StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("transferBySaga", null, businessKey, params);
            if (ExecutionStatus.SU.equals(inst.getStatus())
                    && inst.getCompensationStatus() == null) {
               // If the forward status is successful and the compensation status is null (rollback is not triggered), the transaction is successful.
                return true;
            } else {
               // If the flow reaches the Fail node, an errorCode and an errorMessage are generated.
                String errorCode = (String) inst.getContext().get(DomainConstants.VAR_NAME_STATEMACHINE_ERROR_CODE);
                String errorMessage = (String) inst.getContext().get(DomainConstants.VAR_NAME_STATEMACHINE_ERROR_MSG);
                System.out.println("ErrorCode:" + errorCode + ", ErrorMsg:" + errorMessage + ", exception: " + inst.getException());
                return false;
            }
        } catch (Throwable t) {
            logger.error("Transfer transaction failed", t);
            throw new RuntimeException(t);
        }
    }
}

Participant A (debit) implementation:

public class FirstSagaActionImpl implements FirstSagaAction {
    protected final staticLogger logger = LoggerFactory.getLogger(FirstSagaActionImpl.class);
    // Account DAO
    private AccountDAO firstAccountDAO;
    // Business transaction log DAO
    private AccountTransactionDAO firstAccountTransactionDAO;
    // Transaction template
    private TransactionTemplate firstActionTransactionTemplate;

    /**
     * Debit operation
     **/
    @Override
    public boolean amountMinus(final String businessKey, final String accountNo, final BigDecimal amount, final Map<String, Object> extParams) {
        try {
            if (amount.compareTo(BigDecimal.ZERO) < 0) {
                throw new RuntimeException("Amount must be greater than 0");
            }
            AccountTransaction accountTransaction = firstAccountTransactionDAO.findTransaction(businessKey);
            if (accountTransaction != null && Status.SUCCESS.name().equals(accountTransaction.getStatus())) {
                // Idempotence control: If the transaction is successful, return true.
                return true;
            }
            return firstActionTransactionTemplate.execute(new TransactionCallback<Boolean>() {
                @Override
                public Boolean doInTransaction(TransactionStatus status) {
                    try {
                        // Check the account balance
                        Account account = firstAccountDAO.getAccountForUpdate(accountNo);
                        if (account.getAmount().compareTo(amount) < 0) {
                            throw new RuntimeException("Insufficient balance");
                        }
                        // Record the account transaction log
                        AccountTransaction accountTransaction = new AccountTransaction();
                        accountTransaction.setBusinessKey(businessKey);
                        accountTransaction.setAccountNo(accountNo);
                        accountTransaction.setAmount(amount);
                        accountTransaction.setType(Type.MINUS.name());
                        accountTransaction.setStatus(Status.SUCCESS.name());
                        firstAccountTransactionDAO.addTransaction(accountTransaction);

                        // Debit
                        BigDecimal amount = account.getAmount().subtract(accountTransaction.getAmount());
                        if (amount.compareTo(BigDecimal.ZERO) < 0) {
                            throw new RuntimeException("Insufficient balance");
                        }
                        account.setAmount(amount);
                        firstAccountDAO.updateAmount(account);
                    } catch (Exception e) {
                        logger.error("Debit operation failed", e);
                        throw new RuntimeException("Debit operation failed", e);
                    }
                    return true;
                }
            });
        } catch (Exception e) {
            logger.error("Debit operation failed", e);
            return false;
        }
    }

    /**
     * Compensate (reverse) the debit operation
     **/
    @Override
    public boolean compensateAmountMinus(final String businessKey, final String accountNo) {
        AccountTransaction accountTransaction;
        try {
            accountTransaction = firstAccountTransactionDAO.findTransaction(businessKey);
            if (accountTransaction == null) {
                // The original transaction log does not exist. Record a log to prevent dangling transactions.
                accountTransaction = new AccountTransaction();
                accountTransaction.setBusinessKey(businessKey);
                accountTransaction.setAccountNo(accountNo);
                accountTransaction.setType(Type.MINUS.name());
                accountTransaction.setStatus(Status.COMPENSATED.name());
                firstAccountTransactionDAO.addTransaction(accountTransaction);
                return true;
            }
            if (Status.COMPENSATED.name().equals(accountTransaction.getStatus())) {
                // Idempotence control: If the compensation is successful, return true.
                return true;
            }
            final AccountTransaction accountTransactionFinal = accountTransaction;
            return firstActionTransactionTemplate.execute(new TransactionCallback<Boolean>() {
                @Override
                public Boolean doInTransaction(TransactionStatus status) {
                    try {
                        // Compensate the debited amount
                        Account account = firstAccountDAO.getAccountForUpdate(accountTransactionFinal.getAccountNo());
                        BigDecimal amount = account.getAmount().add(accountTransactionFinal.getAmount());
                        account.setAmount(amount);
                        firstAccountDAO.updateAmount(account);
                        // Update the log status
                        accountTransactionFinal.setStatus(Status.COMPENSATED.name());
                        firstAccountTransactionDAO.updateTransaction(accountTransactionFinal);
                        return true;
                    } catch (Exception e) {
                        logger.error("Debit operation compensation failed", e);
                        status.setRollbackOnly();
                        return false;
                    }
                }
            });
        } catch (SQLException e) {
            logger.error("Debit operation compensation failed", e);
            return false;
        }
    }
}

Implementation for Participant B (Payer):

public class SecondSagaActionImpl implements SecondSagaAction {
    protected final static Logger logger = LoggerFactory.getLogger(SecondSagaActionImpl.class);
    private AccountDAO secondAccountDAO;
    private AccountTransactionDAO secondAccountTransactionDAO;
    private TransactionTemplate secondActionTransactionTemplate;

    /**
     * Credit operation
     **/
    @Override
    public boolean amountAdd(finalString businessKey, final String accountNo, final BigDecimal amount, final Map<String, Object> extParams) {
        try {
            if (amount.compareTo(BigDecimal.ZERO) < 0) {
                throw new RuntimeException("Amount must be greater than 0");
            }
            AccountTransaction accountTransaction = secondAccountTransactionDAO.findTransaction(businessKey);
            if (accountTransaction != null && Status.SUCCESS.name().equals(accountTransaction.getStatus())) {
                // Idempotence control: If the transaction is successful, return true.
                return true;
            }
            return secondActionTransactionTemplate.execute(new TransactionCallback<Boolean>() {
                @Override
                public Boolean doInTransaction(TransactionStatus status) {
                    try {
                        // Record the account transaction log
                        AccountTransaction accountTransaction = new AccountTransaction();
                        accountTransaction.setBusinessKey(businessKey);
                        accountTransaction.setAccountNo(accountNo);
                        accountTransaction.setAmount(amount);
                        accountTransaction.setType(Type.ADD.name());
                        accountTransaction.setStatus(Status.SUCCESS.name());
                        secondAccountTransactionDAO.addTransaction(accountTransaction);

                        Account account = secondAccountDAO.getAccountForUpdate(accountNo);

                        // Credit
                        BigDecimal amount = account.getAmount().add(accountTransaction.getAmount());
                        account.setAmount(amount);
                        secondAccountDAO.updateAmount(account);
                    } catch (Exception e) {
                        logger.error("Credit operation failed", e);
                        throw new RuntimeException("Credit operation failed", e);
                    }
                    return true;
                }
            });
        } catch (Exception e) {
            logger.error("Credit operation failed", e);
            return false;
        }
    }

    /**
     * Compensate (reverse) the credit operation
     **/
    @Override
    public boolean compensateAmountAdd(final String businessKey, final String accountNo) {
        AccountTransaction accountTransaction;
        try {
            accountTransaction = secondAccountTransactionDAO.findTransaction(businessKey);
            if (accountTransaction == null) {
                // Dangling prevention: The original transaction log does not exist. Record a log to prevent dangling transactions.
                accountTransaction = new AccountTransaction();
                accountTransaction.setBusinessKey(businessKey);
                accountTransaction.setAccountNo(accountNo);
                accountTransaction.setType(Type.ADD.name());
                accountTransaction.setStatus(Status.COMPENSATED.name());
                secondAccountTransactionDAO.addTransaction(accountTransaction);

                // Allow null compensation: Return true for successful compensation.
                return true;
            }
            if (Status.COMPENSATED.name().equals(accountTransaction.getStatus())) {
                // Idempotence control: If the compensation is successful, return true.
                return true;
            }

            final AccountTransaction accountTransactionFinal = accountTransaction;
            return secondActionTransactionTemplate.execute(new TransactionCallback < Boolean > () {

                @Override
                public Boolean doInTransaction (TransactionStatus status){
                    try {
                        // Debit the credited amount
                        Account account = secondAccountDAO.getAccountForUpdate(accountTransactionFinal.getAccountNo());
                        BigDecimal amount = account.getAmount().subtract(accountTransactionFinal.getAmount());
                        if (amount.compareTo(BigDecimal.ZERO) < 0) {
                            throw new RuntimeException("Insufficient balance. Please check manually.");
                        }
                        account.setAmount(amount);
                        secondAccountDAO.updateAmount(account);
                        // Update the log status
                        accountTransactionFinal.setStatus(Status.COMPENSATED.name());
                        secondAccountTransactionDAO.updateTransaction(accountTransactionFinal);
                        return true;
                    } catch (Exception e) {
                        logger.error("Credit operation compensation failed", e);
                        status.setRollbackOnly();
                        return false;
                    }
                }
            });
        } catch (SQLException e) {
            logger.error("Credit operation compensation failed", e);
            return false;
        }
    }
}

Hardware environment

The hardware configuration for this scenario is as follows:

Note

RDS uses a MySQL 5.6 database.

hardware

Stress testing data policy

Data scale (number of accounts): This test transfers funds from Group A to Group B to evaluate the performance of the TCC, SAGA, and FMT distributed transaction patterns across different data scales.

Stress testing pattern

Number of accounts in Group A

Number of accounts in Group B

TCC, FMT, SAGA

5,000

5,000

Stress testing results

The following tables show the TPS and response times for the TCC, SAGA, and FMT patterns for a fund transfer from Group A to Group B.

Note

TCC (Total) and TCC (Success) are the total and successful TPS for the TCC pattern, respectively. The same applies to the FMT and SAGA patterns.

  • TPS

    User volume (Group A to Group B)

    TPS

    Group A: 5,000 Group B: 5,000

    Concurrency

    TCC (Total)

    TCC (Success)

    FMT (Total)

    FMT (Success)

    SAGA (Total)

    SAGA (Success)

    1

    50

    50

    40

    40

    48

    48

    5

    250

    250

    200

    200

    240

    240

    10

    460

    460

    380

    380

    450

    450

    20

    800

    800

    695

    690

    780

    780

    50

    1450

    1450

    1220

    1200

    1300

    1300

    100

    1500

    1500

    1550

    1500

    1480

    1480

    200

    1500

    1500

    1600

    1450

    1550

    1550

    500

    1600

    1600

    1500

    1400

    1580

    1580

    700

    1500

    1500

    1500

    1300

    1550

    1550

    5000-tps

  • Response time

    Response Time (Success)

    Concurrency

    TCC

    FMT

    SAGA

    1

    19

    26

    21

    5

    21

    25

    21

    10

    23

    27

    23

    20

    27

    30

    26

    50

    35

    45

    38

    100

    70

    75

    68

    200

    135

    160

    129

    500

    225

    380

    316

    700

    490

    520

    451

    5000-rt