The business scenario

The company has a loan project, whose specific business is similar to Ant Borrow of Ali. Users borrow money on the platform and set a maturity time, within which they need to repay the loan and charge a certain handling fee. If they fail to repay the loan within the specified time, a late fee will be incurred.

Therefore, a loan order will be generated when the user initiates the loan, and the user can repay the loan through Alipay or automatic deduction when the bank card is due in the system. The repayment process goes through the payment system, so whether the user’s repayment is overdue and the number of overdue days and overdue fees are calculated through the system.

But when doing the ordering system, encountered such a business scenario, due to the reason of business allows users through offline alipay payments, we provide a company official alipay qr code, users scan code, then financial don’t time to pull the alipay account under the reimbursement list and generate the standardization of Excel spreadsheet entry to the payment system.

The payment system generates the corresponding payment orders and puts them into storage. Meanwhile, a message is produced to the message system for each repayment record, and the consumer of the message is the order system. The order system will settle the amount of the current user after receiving the message: pay back the principal first, pay off the principal and then pay back the late fee, then pay off the order and increase the loan limit…… , the whole process is roughly as follows:

As can be seen from the above process description, the original online payment is now transferred to offline, which will cause a problem: the payment and settlement is not timely. For example, the user’s order is due on 19-05-27 today, but the user pays it off on 19-05-26. The financial department will take the list from Alipay and input it into the payment system on 19-05-27 or even later. So what happens is that the user is actually paying off the loan in arrears and we’re recording that the user is not paying off the loan and there’s a late fee.

Of course, the above is the business scope of the problem, we are going to talk about today is the payment system to send a message to the order system in a link of a problem. We all know that in order to avoid message loss or order system processing exceptions or network problems, we need to consider message persistence and message failure retry mechanism when designing the message system.

For the retry mechanism, if the order system consumes a message, but the message system does not receive feedback on whether it has been successfully processed due to network problems. The messaging system will be retry periodically according to the configured rules. Yes, it is to ensure normal processing of the system. However, if the network is restored and the first message I received is successfully processed, I receive another message. If no protective measures are taken, the following situation will occur: The user pays once but the order system calculates twice, which will cause the financial bill to be abnormal. Then maybe the users are laughing and the boss is crying.

Interface idempotency

In order to prevent the above situation, we need to provide a protection measure. For the same payment information, if I successfully process one of them, ALTHOUGH I receive the message again, I will not process it at this time, that is, to ensure the idempotency of the interface.

Definition on Wikipedia:

Idempotent (idempotence) is a mathematical and computer concept commonly found in abstract algebra.

** The characteristic of an idempotent operation in programming is that any number of executions have the same effect as a single execution. ** An idempotent function, or idempotent method, is a function that can be executed repeatedly with the same parameters and get the same result. These functions do not affect system state, nor do they have to worry about system changes caused by repeated execution. For example, the “setTrue()” function is an idempotent function that yields the same result no matter how many times it is executed, and the more complex operation idempotent guarantee is implemented with unique transaction numbers (serial numbers).

The central feature of idempotency is that any number of executions have the same effect as a single execution. In fact, the main operation in our programming is CURD, where the Retrieve operation and Delete operation are naturally idempotent, and the ones affected are Create and Update.

In the service, the idempotent is usually repeated requests of the interface. Repeated requests refer to the same request being submitted several times for some reason. There are several scenarios leading to this:

  • Front-end repeated submission: to submit orders, users quickly and repeatedly click for many times, resulting in the backend to generate multiple orders with repeated content.
  • Interface timeout retry: To prevent request loss caused by network jitter or other reasons, the interface that is invoked by a third party is designed to retry multiple times due to timeout.
  • Message repeat consumption: MQ message middleware, message repeat consumption.

Idempotency of interfaces is an issue that must be considered for some business scenarios that have a large impact, such as the interface for money transactions. Otherwise, a wrong, poorly thought out interface could cost the company a lot of money, and the programmers themselves would be the ones to blame.

Idempotence implementation

For interfaces that interact with the Web side, we can intercept some of them in the front end, such as preventing forms from being submitted repeatedly, and putting buttons in grey, hidden and unclickable ways.

However, the actual benefit of front-end control is not very high, understand the point of technology will simulate the request to call your service, so the security strategy still needs to do from the back-end interface layer.

So what are the strategic ways that the back end can achieve idempotency of distributed interfaces? The implementation can be mainly considered from the following aspects:

Token mechanism

In the case of repeated and consecutive clicks in the front end, such as the user shopping and submitting an order, the interface of submitting an order can be implemented through the Token mechanism to prevent repeated submission.

The main process is:

  1. The server provides an interface for sending tokens. When we analyze the business, which business has idempotent problem, we must obtain the token before executing the business, and the server will save the token in Redis. (Microservices must be distributed, if a single machine is suitable for JVM caching).
  2. The token is then carried over when the business interface request is invoked, usually in the header of the request.
  3. The server checks whether the token exists in redis. If the token exists in Redis, the server deletes the token in redis for the first time and continues services.
  4. If the token does not exist in redis, it indicates that the operation is repeated, and the repeated token is directly returned to the client. In this way, the business code will not be executed repeatedly.

Delete tables from the database

Use the unique index feature of the database to ensure unique logic when inserting data into the table. The unique sequence number can be a single field, such as the order number of an order, or a unique combination of multiple fields. For example, design the following database table.

CREATE TABLE `t_idempotent` (
  `id` int(11) NOT NULL COMMENT 'ID'.`serial_no` varchar(255)  NOT NULL COMMENT 'Unique serial number'.`source_type` varchar(255)  NOT NULL COMMENT 'Resource Type'.`status` int(4) DEFAULT NULL COMMENT 'state'.`remark` varchar(255)  NOT NULL COMMENT 'note'.`create_by` bigint(20) DEFAULT NULL COMMENT 'Founder'.`create_time` datetime DEFAULT NULL COMMENT 'Creation time'.`modify_by` bigint(20) DEFAULT NULL COMMENT 'Modifier'.`modify_time` datetime DEFAULT NULL COMMENT 'Modification time',
  PRIMARY KEY (`id`)
  UNIQUE KEY `key_s` (`serial_no`.`source_type`.`remark`)  COMMENT 'Ensure business uniqueness'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Idempotent check table';
Copy the code

Let’s take a look at these key fields,

  • Serial_no: Unique serial number value, in this case I set it by annotation@IdempotentKeyTo identify the fields in the request object and obtain their corresponding values by encrypting them with MD5.
  • Source_type: Business type, differentiating different business, order, payment, etc.
  • Remark: by joining together into a string of identification field, splicing operator for “|”.

As the data establishes a unique index consisting of the combination of serial_no,source_type and remark fields, the idempotency of the interface can be achieved through this method. The specific code design is as follows:

public class PaymentOrderReq {

    /** ** /
    @IdempotentKey(order=1)
    private String alipayNo;

    /**
     * 支付订单ID
     */
    @IdempotentKey(order=2)
    private String paymentOrderNo;

    /**
     * 支付金额
     */
    private Long amount;
}
Copy the code

Since alipay serial number and order number are unique in the system, the unique serial number can be generated by combining them with MD5. The specific generation method is as follows:

private void getIdempotentKeys(Object keySource, Idempotent idempotent) {
    TreeMap<Integer, Object> keyMap = new TreeMap<Integer, Object>();
    for (Field field : keySource.getClass().getDeclaredFields()) {
        if (field.isAnnotationPresent(IdempotentKey.class)) {
            try {
                field.setAccessible(true);
                keyMap.put(field.getAnnotation(IdempotentKey.class).order(),
                        field.get(keySource));
            } catch (IllegalArgumentException | IllegalAccessException e) {
                logger.error("", e);
                return;
            }
        }
    }
    generateIdempotentKey(idempotent, keyMap.values().toArray());
}
Copy the code

Can generate idempotent Key, if there are multiple Key through the separator “|” connection,

private void generateIdempotentKey(Idempotent idempotent, Object... keyObj) {
     if (keyObj.length == 0) {
         logger.info("idempotentkey is empty,{}", keyObj);
         return;
     }
     StringBuilder serialNo= new StringBuilder();
     for (Object key : keyObj) {
         serialNo.append(key.toString()).append("|");
     }
     idempotent.setRemark(serialNo.toString());
     idempotent.setSerialNo(md5(serialNo));
 }
Copy the code

When everything is ready, the interface method of idempotent check can be provided externally. The interface method is:

public <T> void idempotentCheck(IdempotentTypeEnum idempotentType, T keyObj) throws IdempotentException {
    Idempotent idempotent = new Idempotent();
    getIdempotentKeys(keyObj, idempotent );
    if (StringUtils.isBlank(idempotent.getSerialNo())) {
        throw new ServiceException("fail to get idempotentkey");
    }
    idempotentEvent.setSourceType(idempotentType.name());
    try {
        idempotentMapper.saveIdempotent(idempotent);
    } catch (DuplicateKeyException e) {
        logger.error("idempotent check fail", e);
        throw newIdempotentException(idempotent); }}Copy the code

Of course, the appropriate use of this interface method in the project depends on the requirements of the project. You can use the @Autowire annotation to inject where you need to use it, but the disadvantage is that you need to call it everywhere. My personal recommendation is to customize an annotation, apply it to interfaces that require idempotent guarantees, and use it via interceptor methods. This is so simple that there is no code intrusion or contamination.

In addition, the use of database anti-duplicate table has a serious disadvantage, that is, the system fault tolerance is not high, if the database connection of the idempotent table is abnormal or the server is abnormal, it will lead to the whole system idempotent check problems. If you do a database backup to prevent this, it’s an extra hectic task.

Redis implementation

The design and pseudo-code of the anti-duplicate meter have been described above, as well as one obvious disadvantage. So let’s introduce another Redis implementation.

The way Redis is implemented is to use the unique serial number as the Key. The unique serial number is generated in the same way as the anti-duplicate table introduced above. The value can be any information you want to fill in. The unique sequence number can also be a single field, such as the order number of an order, or a unique combination of multiple fields. Of course, you need to set a key expiration time, otherwise there will be too many keys in Redis. The specific verification process is shown in the figure below, and the implementation code is also very simple and will not be written here.

Since enterprises considering using Redis in their projects, since most of them will use it as a cache, it will generally be clustered, and at least two Redis servers will definitely be deployed. So Redis is the best way to implement idempotent interfaces.

The state machine

For many businesses there is a business flow state, and each state has a pre-state, a post-state, and a final end state. For example, the process of pending approval, approval, rejection, re-launch, approval, approval rejected. Order to be submitted, to be paid, paid, cancelled.

Taking orders as an example, the pre-state of a paid state can only be paid, and the pre-state of a cancelled state can only be paid. We can control the idempotence of requests through the flow of this state machine.

public enum OrderStatusEnum {

    UN_SUBMIT(0.0."To be submitted"),
    UN_PADING(0.1."To be paid"),
    PAYED(1.2."Paid for shipment"),
    DELIVERING(2.3."Shipped"),
    COMPLETE(3.4."Done"),
    CANCEL(0.5."Cancelled"),;// In the front state
    private int preStatus;

    / / state values
    private int status;

    // State description
    private String desc;

    OrderStatusEnum(int preStatus, int status, String desc) {
        this.preStatus = preStatus;
        this.status = status;
        this.desc = desc;
    }

    / /...
}
Copy the code

Assuming that the current state is paid, if the payment interface receives another payment request, it will throw an exception or reject the processing.

conclusion

Based on the above understanding, we can know that we need to flexibly choose idempotent realization methods for different business scenarios.

For example to prevent similar to the front end of repeat submit, repeat order scene can be implemented by Token mechanism, and those who have the status of front and rear conversion scenarios can realize idempotence, by means of the state machine for those repeated consumption and interface retry scenario is to use index database is the only way to realize a more reasonable.