概念
接口幂等性指的是同一个接口,多次发出的同一个请求,必须保证操作只执行一次。比如在极短时间内同一个订单用户点击了2次提交,这时第二次点击应视为无效点击。
实现方案举例
基于请求凭证,token机制
此种方式比较常见,大致流程如下:
- 客户端预先向服务端申请一个唯一token作为请求凭证
- 客户端调用接口时,携带token
- 服务端获取客户端token,执行redis SETNX命令将token指定有效期写入redis(key不存在则写入成功,否则失败),若命令执行失败,则表示重复操作,直接返回错误信息给客户端。
数据库唯一索引
此种方式改动量小但和业务密切相关,以新增用户功能为例:
- 给用户表的身份证字段设置非空唯一索引。
- 用户首次点击时由于用户表不存在该身份证号,保存成功。
- 用户重复点击时由于用户表已存在该身份证号,保存失败,程序捕获唯一索引冲突异常。
基于请求体哈希值
此种方式原理类似于token机制, 但比token更加简单,无需修改客户端逻辑,原理如下:
- 通过一定策略辨别出哪些请求是重复的,可以选择给请求体等取SHA256哈希值,SHA256碰撞概率极其低。
- 后端为每个请求生成hash值。
- 若该hash值在指定时间内出现过(可通过redis实现),则返回错误信息给客户端。
- 若该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();
}
}