【安全】Java幂等性校验解决重复点击(6种实现方式)

目录

    • 一、简介
      • 1.1 什么是幂等?
      • 1.2 为什么需要幂等性?
      • 1.3 接口超时,应该如何处理?
      • 1.4 幂等性对系统的影响
    • 二、Restful API 接口的幂等性
    • 三、实现方式
      • 3.1 数据库层面,主键/唯一索引冲突
      • 3.2 数据库层面,乐观锁
      • 3.3 数据库层面,悲观锁(select for update)【不推荐】
      • 3.4 数据库层面,状态机
      • 3.5 应用层面,token令牌【不推荐】
      • 3.6 应用层面,分布式锁【推荐】
    • 四、Java 代码实现
      • 4.1 @NotRepeat 注解
      • 4.2 AOP 切面
      • 4.3 RedisUtils 工具类
      • 4.4 测试类
      • 4.5 测试结果

一、简介

1.1 什么是幂等?

幂等 是一个数学与计算机科学概念,英文 idempotent [aɪˈdempətənt]。

  • 在数学中,幂等用函数表达式就是:f(x) = f(f(x))。比如 求绝对值 的函数,就是幂等的,abs(x) = abs(abs(x))。
  • 计算机科学中,幂等表示一次和多次请求某一个资源应该具有同样的作用

满足幂等条件的性能叫做 幂等性

1.2 为什么需要幂等性?

我们开发一个转账功能,假设我们调用下游接口 超时 了。一般情况下,超时可能是网络传输丢包的问题,也可能是请求时没送到,还有可能是请求到了,返回结果却丢了。这时候我们是否可以 重试 呢?如果重试的话,是否会多赚了一笔钱呢?

在这里插入图片描述

在我们日常开发中,会存在各种不同系统之间的相互远程调用。调用远程服务会有三个状态:成功失败超时

前两者都是明确的状态,但超时则是 未知状态。我们转账 超时 的时候,如果下游转账系统做好 幂等性校验,我们判断超时后直接发起重试,既可以保证转账正常进行,又可以保证不会多转一笔

日常开发中,需要考虑幂等性的场景:

  • 前端重复提交:比如提交 form 表单时,如果快速点击提交按钮,就可能产生两条一样的数据。
  • 用户恶意刷单:例如在用户投票这种功能时,如果用户针对一个用户进行重复提交投票,这样会导致接口接收到用户重复提交的投票信息,会使投票结果与事实严重不符。
  • 接口超时重复提交:很多时候 HTTP 客户端工具都默认开启超时重试的机制,尤其是第三方调用接口的时候,为了防止网络波动等造成的请求失败,都会添加重试机制,导致一个请求提交多次。
  • MQ重复消费:消费者读取消息时,有可能会读取到重复消息。

1.3 接口超时,应该如何处理?

如果我们调用下游接口超时了,我们应该如何处理?其实从生产者和消费者两个角度来看,有两种方案处理:

  • 方案一:消费者角度。在接口超时后,调用下游接口检查数据状态
    • 如果查询到是成功,就走成功流程;
    • 如果是失败,就按失败处理(重新请求)。

在这里插入图片描述

  • 方案二:生产者角度。下游接口支持幂等,上有系统如果调用超时,发起重试即可。

在这里插入图片描述

两种方案都是可以的,但如果是 MQ重复消费的场景,方案一处理并不是很妥当,所以我们还是要求下游系统 对外接口支持幂等

1.4 幂等性对系统的影响

幂等性是为了简化客户端逻辑处理,能防止重复提交等操作,但却增加了服务端的逻辑复杂性和成本,其主要是:

  • 把并行执行的功能改为串行执行,降低了执行效率。
  • 增加了额外控制幂等的业务逻辑,复杂化了业务功能。

在使用前,需要根据实际业务场景具体分析,除了业务上的特殊要求外,一般情况下不需要引入接口的幂等性。

二、Restful API 接口的幂等性

Restful 推荐的几种 HTTP 接口方法中,不同的请求对幂等性的要求不同:

请求类型是否幂等描述
GETGET 方法用于获取资源。一般不会也不应当对系统资源进行改变,所以是幂等的。
POSTPOST 方法用于创建新的资源。每次执行都会新增数据,所以不是幂等的。
PUT不一定PUT 方法一般用于修改资源。该操作分情况判断是否满足幂等,更新中直接根据某个值进行更新,也能保持幂等。不过执行累加操作的更新是非幂等的。
DELETE不一定DELETE 方法一般用于删除资源。该操作分情况判断是否满足幂等,当根据唯一值进行删除时,满足幂等;但是带查询条件的删除则不一定满足。例如:根据条件删除一批数据后,又有新增数据满足该条件,再执行就会将新增数据删除,需要根据业务判断是否校验幂等。

三、实现方式

3.1 数据库层面,主键/唯一索引冲突

日常开发中,为了实现接口幂等性校验,可以这样实现:

  1. 提前在数据库中为唯一存在的字段(如:唯一流水号 bizSeq 字段)添加唯一索引,或者直接设置为主键。
  2. 请求过来,直接将数据插入、更新到数据库中,并进行 try-catch 捕获。
  3. 如果抛出异常,说明为重复请求,可以直接返回成功,或提示请求重复。

补充: 也可以新建一张 防止重复点击表,将唯一标识放到表中,存为主键或唯一索引,然后配合 tra-catch 对重复点击的请求进行处理。

伪代码如下:

/*** 幂等处理*/
Rsp idempotent(Request req){try {insert(req);} catch (DuplicateKeyException e) {//拦截是重复请求,直接返回成功log.info("主键冲突,是重复请求,直接返回成功,流水号:{}",bizSeq);return rsp;}//正常处理请求dealRequest(req);return rsp;
}

3.2 数据库层面,乐观锁

乐观锁:乐观锁在操作数据时,非常乐观,认为别人不会同时在修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下,在此期间是否有人修改了数据。

乐观锁的实现:

就是给表多加一列 version 版本号,每次更新数据前,先查出来确认下是不是刚刚的版本号,没有改动再去执行更新,并升级 version(version=version+1)。

比如,我们更新前,先查一下数据,查出来的版本号是 version=1。

select order_id,version from order where order_id='666'

然后使用 version=1 和 订单ID 一起作为条件,再去更新:

update order set version = version +1status='P' where  order_id='666' and version =1

最后,更新成功才可以处理业务逻辑,如果更新失败,默认为重复请求,直接返回。

流程图如下:

为什么版本号建议自增呢?

因为乐观锁存在 ABA 的问题,如果 version 版本一直是自增的就不会出现 ABA 的情况。

3.3 数据库层面,悲观锁(select for update)【不推荐】

悲观锁:通俗点讲就是很悲观,每次去操作数据时,都觉得别人中途会修改,所以每次在拿数据的时候都会上锁。官方点讲就是,共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其它资源。

悲观锁的实现:

在订单业务场景中,假设先查询出订单,如果查到的是处理中状态,就处理完业务,然后再更新订单状态为完成。如果查到订单,并且不是处理中的状态,则直接返回。

可以使用数据库悲观锁(select … for update)解决这个问题:

begin;  # 1.开始事务
select * from order where order_id='666' for update # 查询订单,判断状态,锁住这条记录
ifstatus !=处理中){//非处理中状态,直接返回;return ;
}
## 处理业务逻辑
update order set status='完成' where order_id='666' # 更新完成
commit; # 5.提交事务

注意:

  • 这里的 order_id 需要是主键或索引,只用行级锁锁住这条数据即可,如果不是主键或索引,会锁住整张表。
  • 悲观锁在同一事务操作过程中,锁住了一行数据。这样 别的请求过来只能等待,如果当前事务耗时比较长,就很影响接口性能。所以一般 不建议用悲观锁的实现方式

3.4 数据库层面,状态机

很多业务表,都是由状态的,比如:转账流水表,就会有 0-待处理,1-处理中,2-成功,3-失败的状态。转账流水更新的时候,都会涉及流水状态更新,即涉及 状态机(即状态变更图)。我们可以利用状态机来实现幂等性校验。

状态机的实现:

比如:转账成功后,把 处理中 的转账流水更新为成功的状态,SQL 如下:

update transfor_flow set status = 2 where biz_seq='666' and status = 1;

流程图如下:

在这里插入图片描述

  • 第1次请求来时,bizSeq 流水号是 666,该流水的状态是处理中,值是 1,要更新为 2-成功的状态,所以该 update 语句可以正常更新数据,sql 执行结果的影响行数是 1,流水状态最后变成了 2。
  • 第2次请求也过来了,如果它的流水号还是 666,因为该流水状态已经变为 2-成功的状态,所以更新结果是0,不会再处理业务逻辑,接口直接返回。

伪代码实现如下:

Rsp idempotentTransfer(Request req){String bizSeq = req.getBizSeq();int rows= "update transfr_flow set status=2 where biz_seq=#{bizSeq} and status=1;"if(rows==1){log.info(“更新成功,可以处理该请求”);//其他业务逻辑处理return rsp;} else if(rows == 0) {log.info(“更新不成功,不处理该请求”);//不处理,直接返回return rsp;}log.warn("数据异常")return rsp:
}

3.5 应用层面,token令牌【不推荐】

token 唯一令牌方案一般包括两个请求阶段:

  1. 客户端请求申请获取请求接口用的token,服务端生成token返回;
  2. 客户端带着token请求,服务端校验token。

流程图如下:

在这里插入图片描述

  1. 客户端发送请求,申请获取 token。
  2. 服务端生成全局唯一的 token,保存到 redis 中(一般会设置一个过期时间),然后返回给客户端。
  3. 客户端带着 token,发起请求。
  4. 服务端去 redis 确认 token 是否存在,一般用 redis.del(token) 的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败,则直接返回结果。

补充: 这种方式个人不推荐,说两方面原因:

  1. 需要前后端联调才能实现,存在沟通成本,最终效果可能与设想不一致。
  2. 如果前端多次获取多个 token,还是可以重复请求的,如果再在获取 token 处加分布式锁控制,就不如直接用分布式锁来控制幂等性了,即下面这种解决方式。

3.6 应用层面,分布式锁【推荐】

分布式锁 实现幂等性的逻辑就是,请求过来时,先去尝试获取分布式锁,如果获取成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。

流程图如下:

  • 分布式锁可以使用 Redis,也可以使用 Zookeeper,不过 Redis 相对好点,比较轻量级。
  • Redis 分布式锁,可以使用 setIfAbsent() 来实现,注意分布式锁的 key 必须为业务的唯一标识
  • Redis 执行设置 key 的动作时,要设置过期时间,防止释放锁失败。这个过期时间不能太短,太短拦截不了重复请求,也不能设置太长,请求量多的话会占用存储空间。

四、Java 代码实现

4.1 @NotRepeat 注解

@NotRepeat 注解用于修饰需要进行幂等性校验的类。

NotRepeat.java

import java.lang.annotation.*;/*** 幂等性校验注解*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NotRepeat {}

4.2 AOP 切面

AOP切面监控被 @Idempotent 注解修饰的方法调用,实现幂等性校验逻辑。

IdempotentAOP.java

import com.demo.util.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;/*** 重复点击校验*/
@Slf4j
@Aspect
@Component
public class IdempotentAOP {/** Redis前缀 */private String API_IDEMPOTENT_CHECK = "API_IDEMPOTENT_CHECK:";@Resourceprivate HttpServletRequest request;@Resourceprivate RedisUtils redisUtils;/*** 定义切面*/@Pointcut("@annotation(com.demo.annotation.NotRepeat)")public void notRepeat() {}/*** 在接口原有的方法执行前,将会首先执行此处的代码*/@Before("notRepeat()")public void doBefore(JoinPoint joinPoint) {String uri = request.getRequestURI();// 登录后才做校验UserInfo loginUser = AuthUtil.getLoginUser();if (loginUser != null) {assert uri != null;String key = loginUser.getAccount() + "_" + uri;log.info(">>>>>>>>>> 【IDEMPOTENT】开始幂等性校验,加锁,account: {},uri: {}", loginUser.getAccount(), uri);// 加分布式锁boolean lockSuccess = redisUtils.setIfAbsent(API_IDEMPOTENT_CHECK + key, "1", 30, TimeUnit.MINUTES);log.info(">>>>>>>>>> 【IDEMPOTENT】分布式锁是否加锁成功:{}", lockSuccess);if (!lockSuccess) {if (uri.contains("contract/saveDraftContract")) {log.error(">>>>>>>>>> 【IDEMPOTENT】文件保存中,请稍后");throw new IllegalArgumentException("文件保存中,请稍后");} else if (uri.contains("contract/saveContract")) {log.error(">>>>>>>>>> 【IDEMPOTENT】文件发起中,请稍后");throw new IllegalArgumentException("文件发起中,请稍后");}}}}/*** 在接口原有的方法执行后,都会执行此处的代码(final)*/@After("notRepeat()")public void doAfter(JoinPoint joinPoint) {// 释放锁String uri = request.getRequestURI();assert uri != null;UserInfo loginUser = SysUserUtil.getloginUser();if (loginUser != null) {String key = loginUser.getAccount() + "_" + uri;log.info(">>>>>>>>>> 【IDEMPOTENT】幂等性校验结束,释放锁,account: {},uri: {}", loginUser.getAccount(), uri);redisUtils.del(API_IDEMPOTENT_CHECK + key);}}
}

4.3 RedisUtils 工具类

RedisUtils.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.concurrent.TimeUnit;/*** redis工具类*/
@Slf4j
@Component
public class RedisUtils {/*** 默认RedisObjectSerializer序列化*/@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 加分布式锁*/public boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);}/*** 释放锁*/public void del(String... keys) {if (keys != null && keys.length > 0) {//将参数key转为集合redisTemplate.delete(Arrays.asList(keys));}}
}

4.4 测试类

OrderController.java

import com.demo.annotation.NotRepeat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Arrays;
import java.util.List;/*** 幂等性校验测试类*/
@RequestMapping("/order")
@RestController
public class OrderController {@NotRepeat@GetMapping("/orderList")public List<String> orderList() {// 查询列表return Arrays.asList("Order_A", "Order_B", "Order_C");// throw new RuntimeException("参数错误");}
}

4.5 测试结果

请求地址:http://localhost:8080/order/orderList

日志信息如下:

在这里插入图片描述

经测试,加锁后,正常处理业务、抛出异常都可以正常释放锁。

整理完毕,完结撒花~ 🌻





参考地址:

1.实战,实现幂等的8种方案!https://blog.csdn.net/sufu1065/article/details/122335349

2.Java中的幂等性,https://blog.csdn.net/JewaveOxford/article/details/103578372

3.Spring Boot 实现接口幂等性的 4 种方案!还有谁不会?https://blog.csdn.net/youanyyou/article/details/114464708

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/134388.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

亚马逊云科技产品测评』活动征文|通过使用Amazon Neptune来预测电影类型初体验

文章目录 福利来袭Amazon Neptune什么是图数据库为什么要使用图数据库什么是Amazon NeptuneNeptune 的特点 快速入门环境搭建notebook 图神经网络快速构建加载数据配置端点Gremlin 查询清理 删除环境S3 存储桶删除 授权声明&#xff1a;本篇文章授权活动官方亚马逊云科技文章转…

chatgpt升级啦,训练数据时间更新到2023年4月,支持tools(升级functionCall),128k上下文

&#xff08;2023年11月7日&#xff09; gpt-4-1106-preview https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo 训练数据日期升级到2023年四月 上线文增加到128k 调用一次chatgpt接口&#xff0c;可以得到多次函数调用 import OpenAI from "openai"…

水利部加快推进小型水库除险加固,大坝安全监测是重点

国务院常务会议明确到2025年前&#xff0c;完成新出现病险水库的除险加固&#xff0c;配套完善重点小型水库雨水情和安全监测设施&#xff0c;实现水库安全鉴定和除险加固常态化。 为加快推进小型水库除险加固前期工作&#xff0c;水利部协调财政部提前下达了2023年度中央补助…

网络流量分类概述

1. 什么是网络流量&#xff1f; 一条网络流量是指在一段特定的时间间隔之内&#xff0c;通过网络中某一个观测点的所有具有相同五元组(源IP地址、目的IP地址、传输层协议、源端口和目的端口)的分组的集合。 比如(10.134.113.77&#xff0c;47.98.43.47&#xff0c;TLSv1.2&…

YOLOv8-Pose推理详解及部署实现

目录 前言一、YOLOv8-Pose推理(Python)1. YOLOv8-Pose预测2. YOLOv8-Pose预处理3. YOLOv8-Pose后处理4. YOLOv8-Pose推理 二、YOLOv8-Pose推理(C)1. ONNX导出2. YOLOv8-Pose预处理3. YOLOv8-Pose后处理4. YOLOv8-Pose推理 三、YOLOv8-Pose部署1. 源码下载2. 环境配置2.1 配置CM…

web前端js基础------制作滚动图片

1&#xff0c;要求 通过定时器使其出现滚动的效果 可以通过按键控制图片滚动的方向&#xff08;设置两个按钮绑定点击事件&#xff09; 当鼠标悬停时图片停止&#xff0c;鼠标离开时图片继续向前滚动&#xff08;可以设置鼠标的悬停和离开事件&#xff09; 参考如下 conten…

揭开堆叠式自动编码器的强大功能

一、介绍 在不断发展的人工智能和机器学习领域&#xff0c;深度学习技术因其处理复杂和高维数据的能力而广受欢迎。在各种深度学习模型中&#xff0c;堆叠式自动编码器是一种多功能且功能强大的工具&#xff0c;可用于特征学习、降维和数据表示。本文探讨了堆叠式自动编码器在深…

【论文阅读】Generating Radiology Reports via Memory-driven Transformer (EMNLP 2020)

资料链接 论文原文&#xff1a;https://arxiv.org/pdf/2010.16056v2.pdf 代码链接&#xff08;含数据集&#xff09;&#xff1a;https://github.com/cuhksz-nlp/R2Gen/ 背景与动机 这篇文章的标题是“Generating Radiology Reports via Memory-driven Transformer”&#xf…

【JAVA】:万字长篇带你了解JAVA并发编程-死锁优化【六】

目录 【JAVA】&#xff1a;万字长篇带你了解JAVA并发编程-并发编程的优化【六】并发编程的优化避免死锁死锁产生的条件避免死锁的方式死锁例程代码使用JpsJstack查看进程死锁问题 避免资源竞争 个人主页: 【⭐️个人主页】 需要您的【&#x1f496; 点赞关注】支持 &#x1f4a…

C#,数值计算——偏微分方程,谱方法的微分矩阵的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// 谱方法的微分矩阵 /// Differentiation matrix for spectral methods /// </summary> public class Weights { public Weights() { …

Spring Boot项目中通过 Jasypt 对属性文件中的账号密码进行加密

下面是在Spring Boot项目中对属性文件中的账号密码进行加密的完整步骤&#xff0c;以MySQL的用户名为root&#xff0c;密码为123321为例&#xff1a; 步骤1&#xff1a;引入Jasypt依赖 在项目的pom.xml文件中&#xff0c;添加Jasypt依赖&#xff1a; <dependency><…

Go语言开发环境安装,hello world!

1. Go开发包SDK https://golang.google.cn/dl/&#xff08;国内也可以安装&#xff09; 根据自己电脑下载对应的安装包&#xff0c;我懒下载了msi安装 然后一路点确定安装Go 2.安装GoLand https://www.jetbrains.com/go/download/#sectionwindows 下载安装包 一路确定安装完…

LoRaWAN物联网架构

与其他网关一样&#xff0c;LoRaWAN网关也需要在规定的工作频率上工作。在特定国家部署网关时&#xff0c;必须要遵循LoRa联盟的区域参数。不过&#xff0c;它是没有通用频率的&#xff0c;每个国家对使用非授权MHZ频段都有不同的法律规定。例如&#xff0c;中国的LoRaWAN频段是…

接口测试工具的实验,Postman、Swagger、knife4j(黑马头条)

一、Postman 最常用的接口测试软件&#xff0c;需要注意点&#xff1a;在进行post请求时&#xff0c;需要选择JSON形式发送 输入JSON字符串&#xff0c;比如&#xff1a; {"maxBehotTime": "2021-04-19 00:19:09","minBehotTime": "2021-…

微信小程序:怎么在一个js中修改另一个js的数据(这里通过缓存进行实现)

实例&#xff1a;现有两个页面index.js和category.js,我现在想在index.js中修改category.js的数据 初始数据 category [{name: 物流配送,list: [{id: 1,job: 外卖骑手,checked: true}, {id: 2,job: 快递员,checked: false}, {id: 3,job: 司机,checked: false}, {id: 4,job: …

Nat. Med. | 基于遗传学原发部位未知癌症的分类和治疗反应预测

今天为大家介绍的是来自Alexander Gusev团队的一篇论文。原发部位未知癌症&#xff08;Cancer of unknown primary&#xff0c;CUP&#xff09;是一种无法追溯到其原发部位的癌症&#xff0c;占所有癌症的3-5&#xff05;。CUP缺乏已建立的靶向治疗方法&#xff0c;导致普遍预后…

支持存档的书签服务LinkWarden

什么是 LinkWarden &#xff1f; Linkwarden 是一个自托管、开源协作书签管理器&#xff0c;用于收集、组织和存档网页。目标是将您在网络上找到的有用网页和文章组织到一个地方&#xff0c;并且由于有用的网页可能会消失&#xff08;参见链接失效的必然性&#xff09;&#xf…

回归模型原理总结及代码实现

前言 本文将介绍回归模型算法&#xff0c;并总结了一些常用的除线性回归模型之外的模型&#xff0c;其中包括一些单模型及集成学习器。 保序回归、多项式回归、多输出回归、多输出K近邻回归、决策树回归、多输出决策树回归、AdaBoost回归、梯度提升决策树回归、人工神经网络、…

Kibana使用Timelion根据时间序列展示数据

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

工业自动化工厂PLC远程控制网关物联网应用

远程控制网关在工厂自动化领域中起到了至关重要的作用&#xff0c;特别是在工厂PLC数据通讯方面。它充当着数据传输的桥梁&#xff0c;连接了工厂中的各类设备和系统&#xff0c;实现了远程监控和控制的功能。本文将详细介绍远程控制网关在工厂PLC数据通讯中的应用。 远程控制网…