接口幂等性探讨

概念

接口幂等性指的是同一个接口,多次发出的同一个请求,必须保证操作只执行一次。比如在极短时间内同一个订单用户点击了2次提交,这时第二次点击应视为无效点击。

实现方案举例

基于请求凭证,token机制

此种方式比较常见,大致流程如下:

  1. 客户端预先向服务端申请一个唯一token作为请求凭证
  2. 客户端调用接口时,携带token
  3. 服务端获取客户端token,执行redis SETNX命令将token指定有效期写入redis(key不存在则写入成功,否则失败),若命令执行失败,则表示重复操作,直接返回错误信息给客户端。

数据库唯一索引

此种方式改动量小但和业务密切相关,以新增用户功能为例:

  1. 给用户表的身份证字段设置非空唯一索引。
  2. 用户首次点击时由于用户表不存在该身份证号,保存成功。
  3. 用户重复点击时由于用户表已存在该身份证号,保存失败,程序捕获唯一索引冲突异常。

基于请求体哈希值

此种方式原理类似于token机制, 但比token更加简单,无需修改客户端逻辑,原理如下:

  1. 通过一定策略辨别出哪些请求是重复的,可以选择给请求体等取SHA256哈希值,SHA256碰撞概率极其低。
  2. 后端为每个请求生成hash值。
  3. 若该hash值在指定时间内出现过(可通过redis实现),则返回错误信息给客户端。
  4. 若该hash值在指定时间内未出现,则指定有效期暂存该hash值(可指定过期时间存入redis),接着执行业务逻辑。

例子:

  • 通过filter拦截请求。
  • 通过对请求体长度+ip+uri+User-Agent取hash256哈希值。
  • 通过判断sha256哈希值是否已在缓存中来辨别请求是否重复。
  • 将sha256哈希值缓存指定时间。
  • 代码如下:
@WebFilter(filterName = "duplicateFilter", urlPatterns = "/*")
public class DuplicateRequestFilter implements Filter {
    //缓存最近2秒内请求的哈希值
    private final TtlMap<String, Long> CONTENT_HASH = new TtlMap<>(100_000, 2_000);

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        //TODO 校验
        if (!needCheck(request) || !checkDuplicateRq(request)){
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        //TODO 输出错误信息
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        writeErrResult(response);
    }

    /** 请求是否需要校验 */
    private boolean needCheck(HttpServletRequest request){
        String method = request.getMethod();

        return !Arrays.asList("GET", "HEAD", "OPTIONS", "TRACE", "CONNECT").contains(method);
    }
    /**
     * @description:通过请求体长度,ip,uri等进行校验
     * @date 17:07 2022/8/31
     * @param request
     * @return true:重复/false:不重复
     **/
    private boolean checkDuplicateRq(HttpServletRequest request) {
        //获取请求uri,ip,客户端类型
        String uri = request.getRequestURI();
        String ip = request.getRemoteAddr();
        String userAgent = request.getHeader("User-Agent");
        //获取请求体长度
        String length = request.getHeader("Content-Length");
        String content = new StringBuilder(ip)
                .append(uri)
                .append(userAgent)
                .append(length).toString();

        //取sha256哈希
        String contentHash = HexUtil.getHexString(content, HexAlgorithmEnum.SHA256);
        boolean hit = CONTENT_HASH.get(contentHash) != null;
        //刷新有限期
        CONTENT_HASH.put(contentHash, System.currentTimeMillis());
        //返回是否命中
        return hit;
    }

    private void writeErrResult(HttpServletResponse response ) throws IOException {
        BaseResultVo resultVo = new BaseResultVo(409, false, "请求重复");
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");

        PrintWriter writer = response.getWriter();
        writer.write( JSON.toJSONString(resultVo) );
        writer.flush();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值