Architecture design | interface idempotence principle, anti duplicate submission Token management

Keywords: Java github Database Programming REST

Source code: GitHub point here || GitEE point here

1, The concept of idempotent

1. An introduction to idempotent

The characteristic of an idempotent operation in programming is that the influence of any multiple execution is the same as that of one execution. That is to say, one or more requests for a resource have the same effect.

2. HTTP request

Follow the Http protocol request, more and more emphasis on Rest request style, can better regulate and understand the interface design.

GET: used to GET resources, without side effects, so it is idempotent;

POST: used to create resources. Repeated POST requests may generate two different resources, with side effects not satisfying idempotence;

PUT: used for update operation. Repeatedly submitting a PUT request will only have side effects on the resources specified in its URL, which satisfies idempotence;

DELETE: used to DELETE resources, with side effects, but it should meet idempotence;

HEAD: it is essentially the same as GET, but the HEAD does not contain rendering data, only HTTP header information, without side effects, and satisfies idempotence;

OPTIONS: used to obtain the request method supported by the current URL, which satisfies idempotence;

2, Scenario business analysis

1. Order payment

In the actual development, we often face the problem of order payment. The basic process is as follows:

  • The client initiates the order payment request;
  • Local related business processing of the system before payment;
  • Request the third party to pay the service for deduction;
  • Third party payment return processing results;
  • The local service responds to the client based on the payment result;

This business process needs to deal with quite complex issues, such as transaction, distributed transaction, interface delay timeout, client repeated submission, etc. here, the process is only viewed from the perspective of idempotent interface, and other issues will be discussed later.

2. Idempotent interface

When the payment request of the above process has a clear result: failure or success, so the business process is easy to handle, but for example, in the payment scenario, if the request times out, how to determine the result status of the service: client request timeout, local service timeout, request payment timeout, payment callback timeout, client response timeout, etc.

This requires the design of process state management.

3. Basic operation case

Simulate the above process and design idempotent interface:

Table structure design

CREATE TABLE `dp_order_state` (
	`order_id` BIGINT (20) NOT NULL AUTO_INCREMENT COMMENT 'order id',
	`token_id` VARCHAR (50) DEFAULT NULL COMMENT 'Anti duplicate submission',
	`state` INT (1) DEFAULT '1' COMMENT '1 Create order, 2 local business, 3 payment business',
	PRIMARY KEY (`order_id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = 'Order status table';

CREATE TABLE `dp_state_record` (
	`id` INT (11) NOT NULL AUTO_INCREMENT COMMENT 'Primary key ID',
	`order_id` BIGINT (20) NOT NULL COMMENT 'order id',
	`state_dec` VARCHAR (50) DEFAULT NULL COMMENT 'Status description',
	PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = 'Status record form';

Simulate business process

Order creation, local business, payment business, separate segment management and submission. Test abnormal circuit breaking business in stages.

@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderStateMapper orderStateMapper ;
    @Resource
    private StateRecordMapper stateRecordMapper ;

    @Override
    public OrderState queryOrder(OrderState orderState) {
        Map<String,Object> paramMap = new HashMap<>() ;
        paramMap.put("order_id",orderState.getOrderId());
        List<OrderState> orderStateList = orderStateMapper.selectByMap(paramMap);
        if (orderStateList != null && orderStateList.size()>0){
            return orderStateList.get(0) ;
        }
        return null ;
    }

    @Override
    public boolean createOrder(OrderState orderState) {
        int saveRes = orderStateMapper.insert(orderState);
        if (saveRes > 0){
            saveStateRecord(orderState.getOrderId(),"Order created successfully");
        }
        return saveRes > 0 ;
    }

    @Override
    public boolean localBiz(OrderState orderState) {
        orderState.setState(2);
        int updateRes = orderStateMapper.updateState(orderState) ;
        if (updateRes > 0){
            saveStateRecord(orderState.getOrderId(),"Local business success");
        }
        return updateRes > 0;
    }

    @Override
    public boolean paymentBiz(OrderState orderState) {
        orderState.setState(3);
        int updateRes = orderStateMapper.updateState(orderState) ;
        if (updateRes > 0){
            saveStateRecord(orderState.getOrderId(),"Payment business success");
        }
        return updateRes > 0;
    }

    private void saveStateRecord (Long orderId,String stateDec){
        StateRecord stateRecord = new StateRecord() ;
        stateRecord.setOrderId(orderId);
        stateRecord.setStateDec(stateDec);
        stateRecordMapper.insert(stateRecord) ;
    }
}

Test interface

According to the order status, the unfinished business is compensated in sections. If the order has been completed, multiple submissions will not affect the final result.

@Api(value = "OrderController")
@RestController
public class OrderController {

    @Resource
    private OrderService orderService ;

    @PostMapping("/submitOrder")
    public String submitOrder (OrderState orderState){
        OrderState orderState01 = orderService.queryOrder(orderState) ;
        if (orderState01 == null){
            // Normal business process
            orderService.createOrder(orderState) ;
            orderService.localBiz(orderState) ;
            orderService.paymentBiz(orderState) ;
        } else {
            switch (orderState01.getState()){
                case 1:
                    // Order created successfully: push back local and payment business
                    orderService.localBiz(orderState01) ;
                    orderService.paymentBiz(orderState01) ;
                    break ;
                case 2:
                    // Order local business succeeded: push payment business later
                    orderService.paymentBiz(orderState01) ;
                    break ;
                default:
                    break ;
            }
        }
        return "success" ;
    }
}

Let's go on: in actual development, this process will not be completed by multiple submission of the page, and the order cannot be submitted repeatedly. Next, we will show how to control it. Here, the business is pushed to completion after execution, or the business may be cleaned up forward, and the whole process is set as failure. Here, it involves critical state judgment. You need to select a state as the identification of success or failure to judge subsequent operations technological process. In the distributed system, the most difficult part of this complex process is the distributed transaction. The final consistency problem will be discussed later.

3, Interface resubmission

1. Form resubmission

In practice, if the interface takes too long to process, the user may click the submit button multiple times, resulting in repeated data.

A common solution: hide a token in the form submission_ The ID parameter is submitted to the interface service together. The database stores the order and the associated tokenId. If the order is submitted multiple times, you can directly return the page prompt information.

2. Demo case

Order associated Token query

@Service
public class OrderServiceImpl implements OrderService {
    @Override
    public Boolean queryToken(OrderState orderState) {
        Map<String,Object> paramMap = new HashMap<>() ;
        paramMap.put("order_id",orderState.getOrderId());
        paramMap.put("token_id",orderState.getTokenId());
        List<OrderState> orderStateList = orderStateMapper.selectByMap(paramMap);
        return orderStateList.size() > 0 ;
    }
}

Test interface

@RestController
public class OrderController {
    @Resource
    private OrderService orderService ;

    @PostMapping("/repeatSub")
    public String repeatSub (OrderState orderState){
        boolean flag = orderService.queryToken(orderState) ;
        if (flag){
            return "Do not submit orders repeatedly" ;
        }
        return "success" ;
    }
}

4, Source code address

GitHub·address
https://github.com/cicadasmile/data-manage-parent
GitEE·address
https://gitee.com/cicadasmile/data-manage-parent

Recommended reading: data and architecture management

No title
A01 Data source management: master-slave dynamic routing, AOP mode read-write separation
A02 Data source management: adapt and manage dynamic data sources based on JDBC mode
A03 Data source management: dynamic permission verification, table structure and data migration process
A04 Data source management: relational database, tabular database and distributed computing
A05 Data source management: PostGreSQL environment integration, JSON type application
A06 Data source management: Based on DataX components, synchronous data and source code analysis
A07 Data source management: OLAP query engine, ClickHouse cluster management
C01 Architecture basis: single service, cluster, distribution, basic difference and connection
C02 Architecture design: Global ID generation strategy in distributed business system
C03 Architecture design: distributed system scheduling, Zookeeper cluster management

Posted by careym1989 on Fri, 22 May 2020 06:59:12 -0700