幂等性与防止重复提交

什么是幂等性

所谓幂等,简单地说,就是对接口的多次调用所产生的结果和调用一次是一致的

那么我们为什么需要接口具有幂等性呢?设想一下以下情形:

  • 在App中下订单的时候,点击确认之后,没反应,就又点击了几次。在这种情况下,如果无法保证该接口的幂等性,那么将会出现重复下单问题。
  • 在接收消息的时候,消息推送重复。如果处理消息的接口无法保证幂等,那么重复消费消息产生的影响可能会非常大。

防止重复提交

前端拦截

前端拦截是指通过 HTML 页面来拦截重复请求,比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态

后端拦截

后端拦截的实现思路是在方法执行之前,先判断此业务是否已经执行过,如果执行过则不再执行,否则就正常执行。

我们将请求的业务 ID 存储在内存中,并且通过添加互斥锁来保证多线程下的程序执行安全,大体实现思路如下图所示:
幂等性与防止重复提交_第1张图片

以下内容仅适用于单机环境下的重复数据拦截

然而,将数据存储在内存中,最简单的方法就是使用 HashMap 存储,或者是使用 Guava Cache 也是同样的效果,但很显然 HashMap 可以更快的实现功能,所以我们先来实现一个 HashMap 的防重(防止重复)版本。
基础版——HashMap

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 普通 Map 版本
 */
@RequestMapping("/user")
@RestController
public class UserController3 {

    // 缓存 ID 集合
    private Map<String, Integer> reqCache = new HashMap<>();

    @RequestMapping("/add")
    public String addUser(String id) {
        // 非空判断(忽略)...
        synchronized (this.getClass()) {
            // 重复请求判断
            if (reqCache.containsKey(id)) {
                // 重复请求
                System.out.println("请勿重复提交!!!" + id);
                return "执行失败";
            }
            // 存储请求 ID
            reqCache.put(id, 1);
        }
        // 业务代码...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}

存在的问题:此实现方式有一个致命的问题,因为 HashMap 是无限增长的,因此它会占用越来越多的内存,并且随着 HashMap 数量的增加查找的速度也会降低,所以我们需要实现一个可以自动“清除”过期数据的实现方案。

Apache 为我们提供了一个 commons-collections 的框架,里面有一个非常好用的数据结构 LRUMap 可以保存指定数量的固定的数据,并且它会按照 LRU 算法,帮你清除最不常用的数据。
首先,我们先来添加 Apache commons collections 的引用:

 

<dependency>
  <groupId>org.apache.commonsgroupId>
  <artifactId>commons-collections4artifactId>
  <version>4.4version>
dependency>

在实际的业务中,我们可能有很多的方法都需要防重,那么接下来我们就来封装一个公共的方法,以供所有类使用:

import org.apache.commons.collections4.map.LRUMap;

/**
 * 幂等性判断
 */
public class IdempotentUtils {

    // 根据 LRU(Least Recently Used,最近最少使用)算法淘汰数据的 Map 集合,最大容量 100 个
    private static LRUMap<String, Integer> reqCache = new LRUMap<>(100);

    /**
     * 幂等性判断
     * @return
     */
    public static boolean judge(String id, Object lockClass) {
        synchronized (lockClass) {
            // 重复请求判断
            if (reqCache.containsKey(id)) {
                // 重复请求
                System.out.println("请勿重复提交!!!" + id);
                return false;
            }
            // 非重复请求,存储请求 ID
            reqCache.put(id, 1);
        }
        return true;
    }
}

调用代码如下:

import com.example.idempote.util.IdempotentUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController4 {
    @RequestMapping("/add")
    public String addUser(String id) {
        // 非空判断(忽略)...
        // -------------- 幂等性调用(开始) --------------
        if (!IdempotentUtils.judge(id, this.getClass())) {
            return "执行失败";
        }
        // -------------- 幂等性调用(结束) --------------
        // 业务代码...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}

来源:https://www.cnblogs.com/vipstone/p/13328386.html

其他思路:

需要进行幂等控制的接口都为修改或新增操作,select查询天然幂等 ,delete删除也是幂等,删除同一个多次效果一样 (因为删除只能删除一次,查询可以查多次)。
新增和修改操作都会有一个编辑的页面,在打开这个编辑页面时就后台生成一个token,可存在Redis或JVM内存,并返回token到浏览器
token可以设置在表单隐藏域,提交新增或修改时查询是否有此token,有可进行操作并删除token,没有就是重复提交(拒绝)。

幂等性与防止重复提交_第2张图片

token校验方式1
token校验方式2

实例

场景要求:页面的数据只能被点击提交一次

发生原因:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交

解决办法:

集群环境:采用 token 加 redis(redis 单线程的,处理需要排队)
单 JVM 环境:采用 token 加 redis 或 token 加 jvm 内存

处理流程:

数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间
提交后后台校验 token,同时删除 token,生成新的 token 返回

客户端解决

一般方法,前端是在点击提交按钮后, 把表单按钮进行置灰

后端 token 加 redis的实现

String itemId = itemModel.getItemId();
if (StringUtils.isNotBlank(itemId)){
// 判断此token是否存在
   if (redisDAO.exists(itemId)){
   // 存在创建冲突 抛出异常
     throw new Exception();
   }
   // 不存在代表首次提交,加进redis,设置过期时间为10s
   redisDAO.setex(item, 10);
}

// rediaDAO还是利用jedisPool进行了封装
// 如:

public boolean exists(String key) {
    try(Jedis jedis = jedisPool.getResource()){
    return jedis.exists(key);
    }
}

public void setex(String key, int seconds, String value){
 try(Jedis jedis = jedisPool.getResource()){
     jedis.setex(key, seconds, value);
    }
}


// 防重复提交
public void stopdubleClick(String key, int seconds){
    if(jedis.exists(key)){
       throw new ReRequestException();
    }
    jedis.setex(key, seconds);
}

你可能感兴趣的:(java,java)