接口幂等性设计

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

幂等性: 对于同一个操作发起一次请求或者多次请求得到的结果都是一样的不会因为请求多次而出现异常现象。

场景:

  • 用户多次请求比如重复点击页面上的按钮
  • 网络异常右移网络原因导致在一定时间内未返回调用成功的信息触发了框架层的重试机制
  • 页面回退都再次提交的动作
  • 程序上的重试机制--对未及时响应的请求发起重试操作

Restful 请求方式的幂等性:

  • POST 相当于新增不具备幂等性
  • GET 对资源的获取。在浏览器中通过地址进行访问每次结果都是一样的天然幂等
  • PUT 将一个资源替换成另一个资源。这是非计算型的更新无论更新多少次结果都是一样的天然幂等
  • DELETE 无论删除多少次都是一样的是天然幂等

如何避免重复提交

1.利用全局唯一ID防止重复提交 

在向数据库新增一条记录时有时会出现错误信息“result in duplicate entry for key primary”原因是插入了相同的ID信息。

利用数据库的主键唯一特性可以解决重复提交问题

流程

  1. 搭建一个生成全局唯一ID的服务可以参考雪花算法SnowFlow进行搭建
  2. 在订单确定页面中调用全局唯一ID服务生成订单号
  3. 提交订单时带上订单号请求到达订单订单系统的下单接口
  4. 订单系统在创建订单信息时订单号使用前端传过来的订单号然后直接将订单信息插入数据库
  5. 如果订单写入成功则是第一次提交返回下单成功如果报ID冲突信息则是重复提交。

 2.利用“Token+Redis”机制防止重复提交

流程:

  1. 订单系统提供一个发放Token的接口。这个Token是一个防重令牌即一串唯一字符串(可以使用uuid)
  2. 在“订单确认页”中调用获取Token的接口该接口向订单确认页返回Token同时将Token写入Redis缓存中并依据实际业务对其设置一定的有效期
  3. 用户在“订单确认页”中点击“提交订单”按钮时将第2步Token以参数或者请求头的形式封装进订单信息然后请求订单系统的下单接口
  4. 下单接口在收到提交下单的请求后首先判断在Redis中是否存在当前传入的Token
  • 如果存在则表示这是第1次请求会删除这个Token继续创建订单的其他业务
  • 如果不存在则表示这不是第1此请求而是重复的请求会终止后面的业务逻辑

代码实现

生成Token

package com.wxclient.controller;


import com.fasterxml.uuid.Generators;
import com.yl.entitys.RespEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/system/idempotence")
public class IdempotenceController {

    @Autowired
    private RedisTemplate redisTemplate;

    public static final String USER_TOKEN_PREFIX = "idempotence:token:";
    /**
     * 参数token , 放入redis set数据结构防止重复返回给前端做接口幂等性
     */
    @GetMapping("/{userId}")
    public RespEntity idempotence(@PathVariable Integer userId) {
        // 基于时间的UUID全球唯一
        UUID uuid = Generators.timeBasedGenerator().generate();
        // 将token 放入redis set中 ,5分钟的过期时间
        redisTemplate.opsForValue().set(USER_TOKEN_PREFIX+userId, uuid,5, TimeUnit.MINUTES);
        log.debug("【系统日志】产生的TOKEN->{}", uuid);
        return RespEntity.okData(uuid);
    }

}

 业务检验

	 /**
     * 添加公告信息
     */
    @ApiOperation(value = "添加公告信息")
    @PostMapping("/")
    public RespEntity add(@RequestBody YlNotice notice, HttpServletRequest httpServletRequest) {
        log.debug("【系统日志】添加公告信息---》");
        // 验证幂等性的标识
        String idempotence = httpServletRequest.getHeader("idempotence");
        // 用户的JWT信息
        String jwt = httpServletRequest.getHeader("jwt");

        JWT token = JWTUtil.parseToken(jwt);
        Integer userId = (Integer) token.getPayload("id");
        // 获取用户ID
        log.debug("【系统日志】用户{}->",userId);
        log.debug("【幂等性】idempotence{}->",idempotence);
        //获取redis中的令牌【令牌的对比和删除必须保证原子性】
        //LUA脚本  返回0表示校验令牌失败  1表示删除成功校验令牌成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long result = (Long) redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Arrays.asList(USER_TOKEN_PREFIX + userId),
                idempotence);
        if (result == 1) {
            log.debug("【幂等性】OK{}->",idempotence);
            log.debug("【系统日志】redis验证成功{}->",idempotence);
            //令牌验证成功
            //去创建、下订单、验令牌、验价格、锁定库存...
            if (notice.getNoticeTitlet().isEmpty()){
                return new RespEntity(501, "公告标题不能为空", null);
            }
            if (notice.getNoticeContent().isEmpty()){
                return new RespEntity(501, "公告内容不能为空", null);
            }

            // 默认启用
            notice.setNoticeStates("y");
            LocalDateTime dateTime = LocalDateTime.now(); // 获取当前时间
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
            notice.setNoticeTime(dateTime.format(formatter));
            noticeService.save(notice);
            return RespEntity.SUCCESS;
        } else {
            log.debug("【幂等性】ERROR{}->",idempotence);
            log.debug("【系统日志】redis验证失败{}->",idempotence);
            //令牌校验失败返回失败信息
            return RespEntity.FAIL;
        }
    }

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6