How to ensure the idempotence of the interface in order business

In our order business, how to ensure the idempotence of the interface

1. Token mechanism

1. The server provides an interface for sending tokens. When we analyze the business, which businesses have idempotent problems, we must obtain the token before executing the business, and the server will save the token in redis

2. Then when calling the business interface request, carry the token, and generally put it in the header of the request

3. The server judges whether there is redis in the token. Existence means the first request, and then deletes the token and continues to execute the business

4. If it is judged that the token does not exist in redis, it means that the operation is repeated, and the repeated mark is directly returned to the client, which ensures that the business code will not be executed repeatedly

Dangerous:

1. Delete the token first or delete the token later:

Deleting first may result in that the business is indeed not executed. Retrying has to bring the previous token. Due to the anti-repetition design, the request cannot be executed and
then the deletion may cause the business to be processed successfully, but the service is interrupted, timeout occurs, and it is not deleted. Token, others continue to retry, causing the business to be executed twice.
We finally designed to delete the token first. If the business call fails, get the token again. Request again
2. Token acquisition, comparison and deletion must be atomic

redis.get (token), token.equals, redis.del (token), if these two operations are not atomic, it may cause that under high concurrency, all get the same data, the judgment is successful, and the business continues to execute concurrently
This operation can be done in redis using lua script
"if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"

2. Various lock mechanisms

1. Pessimistic database lock
select * from xxx where id = 1 for update;

Lock this record when querying for update, others need to wait

Pessimistic locks are generally used together with transactions. The data lock time may be very long and needs to be selected according to the actual situation. In addition, it should be noted that the id field must be a primary key or a unique index, otherwise it may cause the result of the lock table and process it. Will be very troublesome

2. Optimistic locking of the database
This method is suitable for update scenarios

update t_goods set count = count - 1,version = version + 1 where good_id = 2 and version = 1

According to the version version, that is, get the version number of the current product before operating the data inventory, and then bring the version version number when operating. Let’s sort out, when we operate the inventory for the first time, we have

When the version is 1, the inventory service version = 2 is called, but there is a problem returning to the order service. The order service calls the inventory service again. When the version passed by the order service is still 1, execute the above

The sql statement will not be executed because the version has become 2 and the where condition is not established. This ensures that no matter how many times it is called, it will only be processed once. Optimistic locking is mainly used to deal with the problem of more reads and less writes

3. Business layer distributed lock.
If multiple machines may process the same data at the same time, for example, multiple machines get the same data in a timed task, we can add a distributed lock to lock this data, and release the lock after the processing is completed. To acquire the lock, you must first determine whether the data has been processed

3. Various unique constraints

1. The unique constraint of the database. When
inserting data, it should be inserted according to the unique index, such as the order number. It is impossible to insert two orders for the same order. We prevent duplication at the database level

This mechanism takes advantage of the unique constraint of the primary key of the database and solves the idempotent problem in the insert scenario, but the requirement for the primary key is not an auto-incremented primary key, so the business needs to generate a globally unique primary key

If it is a sub-database and sub-table scenario, the routing rules should ensure that the same request is landed in the same database and the same table, otherwise the database primary key constraint will not be effective, because the primary keys of different databases and tables are not related

2. Redis set anti-weighting. A
lot of data needs to be processed and can only be processed once. For example, we can calculate the MD5 of the data and put it into redis.

set, each time the data is processed, first check whether the MD5 already exists, and do not process it if it exists

4. Anti-weight table

Use the order table orderNo as the unique index of the de-duplication table, insert the unique index into the de-duplication table, and then perform business operations, and they are in the same transaction, so that repeated requests are guaranteed because the de-duplication table is unique

Constraints, resulting in request failure, avoiding idempotence and other issues, the deduplication table and business table should be in the same database, so as to ensure that in the same transaction, even if the business operation fails, the data in the deduplication table will be returned Get off this

A good guarantee of data consistency,

Redis anti-weight is also a token mechanism

5. Global request unique id

When the interface is called, a unique id is generated, redis saves the data in the collection (de-duplication), and it is processed when it exists. You can use nginx to set a unique id for each request

proxy_set_header X-Request-Id $Request_id。

What we demonstrate here is the token mechanism as follows

Insert picture description here
When you click to settle and jump to the order settlement page, you must generate an idempotent anti-repeated token on the back-end and send it back to the front-end, and then pass it to the back-end for verification when the order is submitted.
Insert picture description here

    /**
     * 去结算
     * 给订单确认ye返回数据
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    
    

        //要返回的大对象
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //在TreadLocal中获取用户
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

        //Feign + 异步任务 需要共享RequestAttributes 每个任务都要setRequestAttributes()
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
    
    
            //每一个线程都要共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1 远程查询大对象的第一个属性 收货地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResVo.getId());
            confirmVo.setAddress(address);
            //Feign在远程调用之前要构造请求,调用很多的拦截器
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
    
    
            //每一个线程都要共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2 远程查询大对象的第二个属性 所有购物项
            List<OrderItemVo> items = cartFeignService.currentUserItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(() -> {
    
    
            List<OrderItemVo> items = confirmVo.getItems();
            List<Long> ids = items.stream().map((item) -> {
    
    
                return item.getSkuId();
            }).collect(Collectors.toList());
            //远程查看库存
            R r = wareFeignService.getSkuHasStock(ids);
            List<SkuStockVo> skuStockVos = r.getData(new TypeReference<List<SkuStockVo>>() {
    
    
            });
            if (skuStockVos != null) {
    
    
                Map<Long, Boolean> booleanMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(booleanMap);
            }
        }, executor);

        //3 远程查询用户积分
        Integer integration = memberResVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4 其他的数据自动计算

        //TODO 5 防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        //给服务器一个 并指定过期时间
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId(), token, 30, TimeUnit.MINUTES);
        //给页面一个
        confirmVo.setOrderToken(token);

        CompletableFuture.allOf(getAddressFuture, cartFuture).get();

        return confirmVo;
    }

When we click to submit an order, an order will be generated, and then we will be redirected to the payment page.
When generating orders, in the future, to ensure the idempotence of the interface to avoid repeated submission of orders, we need to pass the anti-repeated token token for verification. Here, when verifying the atomic token, a script is used to ensure that the comparison and deletion are atomic Sexual. Similar to a distributed lock, but here
//1, first verify the token
//0 failed-1 succeeded | does not exist 0 exists to delete? 1:0
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

/**
     * 提交订单 去支付
     *
     * @Transactional 是一种本地事物,在分布式系统中,只能控制住自己的回滚,控制不了其他服务的回滚
     * 分布式事物 最大的原因是 网络问题+分布式机器。
     * (isolation = Isolation.REPEATABLE_READ) MySql默认隔离级别 - 可重复读
     */
    //分布式事务 全局事务
    //@GlobalTransactional 不用
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
    
    

        //共享前端页面传过来的vo
        confirmVoThreadLocal.set(vo);

        //要返回到大对象
        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        //登录的用户
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();
        responseVo.setCode(0);
        //1、首先验证令牌
        //0失败 - 1成功 | 不存在0 存在 删除?1:0
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        String orderToken = vo.getOrderToken();
        //原子验证令牌 和 删除令牌
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()),
                orderToken);
        if (result == 0) {
    
    
            //令牌验证失败 非0失败码
            responseVo.setCode(1);
            return responseVo;
        } else {
    
    
            //令牌验证成功 -> 执行业务代码
            //下单 去创建订单 验证令牌 验证价格 锁库存
            //TODO 3 保存订单
            OrderCreatTo orderCreatTo = creatOrder();
            BigDecimal payAmount = orderCreatTo.getOrder().getPayAmount();
            BigDecimal payPrice = vo.getPayPrice();
            //金额对比
            if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
    
    
                //保存到数据库
                saveOrder(orderCreatTo);
                //库存锁定 只要有异常就回本订单数据
                WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
                wareSkuLockVo.setOrderSn(orderCreatTo.getOrder().getOrderSn());
                List<OrderItemVo> collect = orderCreatTo.getOrderItems().stream().map((item) -> {
    
    
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    orderItemVo.setTitle(item.getSkuName());
                    return orderItemVo;
                }).collect(Collectors.toList());
                wareSkuLockVo.setLocks(collect);
                /**
                 * TODO 4 远程锁库存 非常严重
                 * 库存成功了,但是网络原因超时了,订单可以回滚,库存不能回滚
                 * 为了保证高并发,库存需要自己回滚。 这样可以采用发消息给库存服务
                 * 库存服务本身也可以使用自动解锁模式 使用消息队列完成  使用延时队列
                 */
                R r = wareFeignService.orderLockStock(wareSkuLockVo);
                if (r.getCode() == 0) {
    
    
                    //锁成功
                    responseVo.setOrder(orderCreatTo.getOrder());
                    //TODO 5 远程扣减积分
                    //库存成功了,但是网络原因超时了,订单回滚,库存不回滚
//                    int i = 1 / 0;//模拟积分系统异常
                    //TODO 订单创建成功,发消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", orderCreatTo.getOrder());
                    return responseVo;
                } else {
    
    
                    //锁定失败
                    String msg1 = (String) r.get("msg");
                    throw new NoStockException(msg1);
                }
            } else {
    
    
                responseVo.setCode(2);
                return responseVo;
            }
        }
    }

Guess you like

Origin blog.csdn.net/u014496893/article/details/114186531