1. 前言🔥
在日常业务开发中,处理重复请求应该是我们需要经常注意的,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些请求是涉及写入操作的,一旦重复了,很可能会导致很严重的后果,例如交易的接口如果重复请求就可能会重复下单。还比如如下场景:
-
黑客拦截了请求,重放
-
前端/客户端因为某些原因重复请求了,或者用户在很短的时间内多次点击请求。
-
网关重发
-
….
那么在Spring Boot 中,防止重复请求的方法有那些?像如何禁止用户重复点击等客户端操作将不在本文的讨论范畴(有点low),我要玩点高级的,同学们请看:
-
Token 验证
解析:在页面中生成一个唯一的Token,然后在请求中携带此Token,服务端接收到请求后验证解析该Token是否是正确的。如果Token不正确,则认为是重复请求并过滤/拒绝该次请求。
-
Token 桶算法
解析:在服务端中使用Token桶算法对请求进行限制,每个用户都有一个Token桶,每次请求需要从Token桶中获取一个Token,如果Token桶中没有用户此次请求的Token,则认为是重复请求并过滤/拒绝该次请求。
-
限流控制
解析:通过在请求接口中设置一个时间间隔,例如5秒钟,同一个用户在5秒钟内只能请求一次,如果再次请求则认为是重复请求并过滤/拒绝该次请求。
-
接口密等性设计
解析:通过设计接口的幂等性来防止重复请求。在设计接口时,确保同样的请求不管发送多少次都会得到相同的结果,这样即使用户发送了重复请求,服务端都返回同一种请求结果,也就不会对系统及数据造成影响。
那么,具体如何一一实现呢?这将又会是干货满满的一期,全程无尿点不废话只抓重点教,具有非常好的学习效果,拿好小板凳准备就坐!希望学习的过程中大家认真听好好学,学习的途中有任何不清楚或疑问的地方皆可评论区留言或私信,bug菌将第一时间给予解惑,那么废话不多说,直接开整!Fighting!!
2. 环境说明🔥
本地的开发环境:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 1.8
- Spring Boot版本:2.3.1 RELEASE
- Maven版本:3.8.2
3. 正文🔥
那么接下来,我就给同学们逐一具体介绍下如上四种防止重复请求的解决方案吧。
3.1 Token验证
3.1.1 思路
在页面中生成一个唯一的Token,然后在请求中携带此Token,服务端接收到请求后验证解析该Token是否是正确的。如果Token不正确,则认为是重复请求并过滤/拒绝该次请求。
3.1.2 代码示例
①首先在页面中生成一个唯一的Token
<form id="demoForm" action="/submit" method="post">
<input type="hidden" name="token" value="${token}" />
<!-- 其他表单元素 -->
<button type="submit">提交</button>
</form>
②在服务端中接收请求并验证Token
@RestController
public class DemoController{
private Map<String, Boolean> tokenMap = new ConcurrentHashMap<>();
@PostMapping("/submit")
public String submit(HttpServletRequest request) {
//获取请求携带的参数token值
String token = request.getParameter("token");
//校验是否存有
if (!tokenMap.containsKey(token)) {
//不存在则放过,存在则判定同一次请求
tokenMap.put(token, true);
// 处理请求
return "success";
} else {
// 重复请求
return "error";
}
}
}
3.2 Token桶算法
3.2.1 思路
在服务端中使用Token桶算法对请求进行限制,每个用户都有一个Token桶,每次请求需要从Token桶中获取一个Token,如果Token桶中没有用户此次请求的Token,则认为是重复请求并过滤/拒绝该次请求。
3.2.2 代码示例
@RestController
public class DemoController {
private Map<String, LinkedList<Long>> tokenBucketMap = new ConcurrentHashMap<>();
@PostMapping("/submit")
public String submit(HttpServletRequest request) {
//获取用户id
String userId = request.getParameter("userId");
//通过用户id生成一个token桶
LinkedList<Long> tokenBucket = tokenBucketMap.get(userId);
if (tokenBucket == null) {
tokenBucket = new LinkedList<>();
tokenBucketMap.put(userId, tokenBucket);
}
long currentTime = System.currentTimeMillis();
synchronized (tokenBucket) {
if (tokenBucket.size() < 10 || currentTime - tokenBucket.getFirst() > 60000) {
tokenBucket.addLast(currentTime);
if (tokenBucket.size() > 10) {
tokenBucket.removeFirst();
}
// 处理请求
return "success";
} else {
// 重复请求
return "error";
}
}
}
}
3.3 限流控制
3.3.1 思路
通过在请求接口中设置一个时间间隔,例如5秒钟,同一个用户在5秒钟内只能请求一次,如果再次请求则认为是重复请求并过滤/拒绝该次请求。
3.3.2 代码示例
@RestController
public class DemoController {
private Map<String, Long> lastRequestTimeMap = new ConcurrentHashMap<>();
@PostMapping("/submit")
public String submit(HttpServletRequest request) {
//获取请求中的用户id
String userId = request.getParameter("userId");
//从map中查找上次请求的时间戳
Long lastRequestTime = lastRequestTimeMap.get(userId);
//如果为空或者上次请求的时间戳与当前时间做差大于5s,则视为新请求,否则重复请求。
if (lastRequestTime == null || System.currentTimeMillis() - lastRequestTime > 5000) {
lastRequestTimeMap.put(userId, System.currentTimeMillis());
// 处理请求
return "success";
} else {
// 重复请求
return "error";
}
}
}
3.4 接口密等性设计
3.4.1 思路
通过设计接口的幂等性来防止重复请求。在设计接口时,确保同样的请求不管发送多少次都会得到相同的结果,这样即使用户发送了重复请求,服务端都返回同一种请求结果,也就不会对系统及数据造成影响。
3.4.2 代码示例
@RestController
public class DemoController {
private Map<String, String> resultCache = new ConcurrentHashMap<>();
@PostMapping("/submit")
public String submit(HttpServletRequest request) {
//指定一个请求参数key。
String key = request.getParameter("key");
//将请求结果存放到key值里
String result = resultCache.get(key);
//如果result不为空则说明是重复请求。
if (result != null) {
// 返回之前的结果
return result;
} else {
// 处理请求并缓存结果
// 模拟一个结果赋值
result = doBusinessLogic();
// 将结果缓存
resultCache.put(key, result);
// 返回
return result;
}
}
private String doBusinessLogic() {
// 业务逻辑处理
return "success";
}
}
好啦,以上就是4种解决重复请求的解决方式啦,同学们觉得那种更好可以在文末进行投票讨论交流哦。
4. 热文推荐🔥
滴~如下推荐【Spring Boot 进阶篇】的学习大纲,请小伙伴们注意查收。
Spring Boot进阶(01):Spring Boot 集成 Redis,实现缓存自由
Spring Boot进阶(02):使用Validation进行参数校验
Spring Boot进阶(03):如何使用MyBatis-Plus实现字段的自动填充
Spring Boot进阶(04):如何使用MyBatis-Plus快速实现自定义sql分页
Spring Boot进阶(05):Spring Boot 整合RabbitMq,实现消息队列服务
Spring Boot进阶(06):Windows10系统搭建 RabbitMq Server 服务端
Spring Boot进阶(07):集成EasyPoi,实现Excel/Word的导入导出
Spring Boot进阶(08):集成EasyPoi,实现Excel/Word携带图片导出
Spring Boot进阶(09):集成EasyPoi,实现Excel文件多sheet导入导出
Spring Boot进阶(10):集成EasyPoi,实现Excel模板导出成PDF文件
Spring Boot进阶(11):Spring Boot 如何实现纯文本转成.csv格式文件?
Spring Boot进阶(12):Spring Boot 如何获取Excel sheet页的数量?
Spring Boot进阶(13):Spring Boot 如何获取@ApiModelProperty(value = “序列号“, name = “uuid“)中的value值name值?
Spring Boot进阶(14):Spring Boot 如何手动连接库并获取指定表结构?一文教会你
Spring Boot进阶(15):根据数据库连接信息指定分页查询表结构信息
Spring Boot进阶(16):Spring Boot 如何通过Redis实现手机号验证码功能?
Spring Boot进阶(17):Spring Boot如何在swagger2中配置header请求头等参数信息
Spring Boot进阶(18):SpringBoot如何使用@Scheduled创建定时任务?
Spring Boot进阶(19):Spring Boot 整合ElasticSearch
Spring Boot进阶(20):配置Jetty容器
Spring Boot进阶(21):配置Undertow容器
Spring Boot进阶(22):Tomcat与Undertow容器性能对比分析
Spring Boot进阶(23):实现文件上传
Spring Boot进阶(24):如何快速实现多文件上传?
Spring Boot进阶(25):文件上传的单元测试怎么写?
Spring Boot进阶(26):Mybatis 中 resultType、resultMap详解及实战教学
Spring Boot进阶(27):Spring Boot 整合 kafka(环境搭建+演示)
Spring Boot进阶(28):Jar包Linux后台启动部署及滚动日志查看,日志输出至实体文件保存
Spring Boot进阶(29):如何正确使用@PathVariable,@RequestParam、@RequestBody等注解?不会我教你,结合Postman演示
Spring Boot进阶(30):@RestController和@Controller 注解使用区别,实战演示
...
5. 文末🔥
如果想系统性的学习Spring Boot,小伙伴们直接订阅bug菌专门为大家创建的Spring Boot专栏《滚雪球学Spring Boot》从入门到精通,从无到有,从零到一!以知识点+实例+项目的学习模式由浅入深对Spring Boot框架进行学习&使用。
如果你有一定的基础却又想精进Spring Boot,那么《Spring Boot进阶实战》将会是你的最好的选择;此栏进行知识点+实例+项目的学习方式全面深入框架剖析及各种高阶玩法,励志打造全网最全最新springboot学习专栏,投资学习自己性价比最高。
我是bug菌,一名想走👣出大山改变命运的程序猿。接下来的路还很长,都等待着我们去突破、去挑战。来吧,小伙伴们,我们一起加油!未来皆可期,fighting!
关注公众号,获取最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等硬核资源