所谓幂等,简单地说,就是对
接口的多次调用所产生的结果和调用一次是一致的
。
那么我们为什么需要接口具有幂等性呢?设想一下以下情形:
前端拦截是指通过 HTML 页面来拦截重复请求,比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态
。
后端拦截的实现思路是在方法执行之前,先判断此业务是否已经执行过,如果执行过则不再执行,否则就正常执行。
我们将请求的业务 ID 存储在内存中,并且通过添加互斥锁来保证多线程下的程序执行安全,大体实现思路如下图所示:
以下内容仅适用于单机环境下的重复数据拦截
然而,将数据存储在内存中,最简单的方法就是使用 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,没有就是重复提交(拒绝)。
token校验方式1
token校验方式2
发生原因:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交
集群环境:采用 token 加 redis(redis 单线程的,处理需要排队)
单 JVM 环境:采用 token 加 redis 或 token 加 jvm 内存
数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间
提交后后台校验 token,同时删除 token,生成新的 token 返回
一般方法,前端是在点击提交按钮后, 把表单按钮进行置灰
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);
}