
网上对于token的实现大部分是在第一阶段进行redis存入这个token,然后将这个token返回给前端,在第二个请求的时候携带这个token

我仅在这里表名一下我的想法,欢迎大家指正
我认为这种做法并不是很好,因为它的侧重点不在保存上,而在创建订单上,而且它创建订单的时候就进行redis保存token,可能会造成token的无用存储(不保存)
我的方案是恰好相反,也就是直接在第二步的时候进行token的保存,第一步只返回一个随机的UUID(也可以自定义)

是否所有的接口都需要做到接口幂等性取决于具体的业务需求和场景。接口幂等性虽然是设计接口时的一种良好实践,但并非所有情况下都是必须的。以下是对这一问题的分析:
- **创建订单、支付等关键操作**:当用户提交订单或进行支付时,如果因网络问题或其他原因导致请求被重复发送,系统应该能够识别这些重复请求并避免产生重复的订单或支付。此类操作通常需要幂等性。
- **更新操作**:例如更新用户信息、修改配置等操作。幂等性确保多次相同的请求只会对系统状态产生一次性影响,避免因重复操作导致数据不一致。- **公共API**:如果你的接口是对外公开的API,可能会有多种不同的客户端调用它们。为了保证系统的稳定性和安全性,确保接口幂等性是非常必要的。- 在一些不稳定的网络环境中,重复请求很容易发生。幂等性可以防止由于网络重试导致的数据问题。- **GET 请求**:通常用于查询数据的GET请求天然是幂等的,无论执行多少次查询操作,都不会改变系统的状态。因此,这类请求不需要特别设计幂等性。- **日志记录、访问统计等**:例如向数据库中写入日志信息、更新访问统计次数等,这些操作即使重复多次,也不会对系统的核心数据产生重大影响。因此,不一定需要做到幂等。- 如果接口仅用于内部服务之间的通信,并且调用逻辑受控(如在事务中执行的操作),则不一定需要特别考虑幂等性。因此,虽然并非所有接口都需要实现幂等性,但对于很多关键接口来说,幂等性仍然是必要的。如果每个接口都单独实现生成幂等令牌的逻辑,显得有些重复和冗余。
package fun.acowbo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author xiaobo
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// 模块名称
String module() default "";
// Token 过期时间,单位为分钟
long expiryTime() default 10;
}注解中设置这两个参数 module 和 expiryTime,具有以下好处:
module** 参数):- **清晰的业务区分**:通过指定模块名称,可以在处理幂等性逻辑时,对不同的业务模块进行区分。这有助于根据不同模块的需求进行定制化处理,避免全局统一处理带来的灵活性不足。
- **易于维护**:在项目扩展时,可以方便地查看和管理不同模块的幂等性设置,增强代码的可维护性和可读性。- **动态调整**:通过设置不同的方法过期时间,允许根据业务需求动态调整幂等令牌的有效期。例如,对于某些操作,幂等性可能只需要短时间内有效,而对于另一些操作,则可能需要更长的有效期。
- **安全性提升**:过期时间限制了幂等令牌的生命周期,降低了长期存在的幂等令牌可能被滥用的风险,进一步增强了接口的安全性。这两个参数使得幂等性控制更加灵活和可配置,适应了不同的业务需求和安全要求。
package fun.acowbo.aspect;
import fun.acowbo.annotation.Idempotent;
import fun.acowbo.utils.BoRedisUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="https://acowbo.fun">acowbo</a>
* @since 2024/9/3
*/
@Aspect
@Component
public class IdempotentAspect {
@Resource
private HttpServletRequest request;
@Around("@annotation(idempotent)")
public Object handleIdempotency(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 获取请求 Token(例如从请求头或方法参数中获取)
String token = extractTokenFromRequest();
// 检查 Token 是否已存在
boolean tokenStored = BoRedisUtil.setIfAbsent(idempotent.module() + ":" + token, "1", idempotent.expiryTime(), TimeUnit.MINUTES);
if (!tokenStored) {
// 如果 Token 已存在,则表示请求重复,返回错误或结果
throw new RuntimeException("订单已经存在了!");
}
// 如果 Token 存储成功,则执行目标方法
return joinPoint.proceed();
}
private String extractTokenFromRequest() {
// 从请求头中提取 Token
String token = request.getHeader("Idempotency-Token");
if (token == null || token.isEmpty()) {
throw new RuntimeException("没有请求token");
}
return token;
}
}虽然这段代码看似简单,但需要特别强调的是 setIfAbsent 方法,它实际上是 Redis 的 SETNX 命令。这是一种原子操作,意味着在高并发的情况下,多个线程或请求同时执行 setIfAbsent 时,Redis 会确保只有第一个请求能够成功设置键值,其余的都会失败。正是因为这种原子性,才保证了幂等性控制在并发环境下的可靠性,避免了重复提交的问题。
package fun.acowbo.controller;
import fun.acowbo.annotation.Idempotent;
import fun.acowbo.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* @author <a href="https://acowbo.fun">acowbo</a>
* @since 2024/9/3
*/
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@GetMapping("/create/{orderNo}")
@Idempotent(module = "order", expiryTime = 3)
public String createOrder(@PathVariable String orderNo) {
return orderService.createOrder(orderNo);
}
}package fun.acowbo.service;
import org.springframework.stereotype.Service;
/**
* @author <a href="https://acowbo.fun">acowbo</a>
* @since 2024/9/3
*/
@Service
public class OrderService {
public String createOrder(String orderNo) {
// 处理创建订单的逻辑
// 此处省略实际的业务处理代码
return "订单创建成功: " + orderNo;
}
}这里我使用的是wrk进行的测试,感兴趣的可以看我下面这篇文章

上图可以看出我进行压测是用10个线程100并发持续了30s16157个请求,失败了16156个,也就是仅有一个成功了。
在数字化时代,用户体验和系统稳定性是每个开发者都必须关注的核心问题。想象一下,如果你的应用在用户点击一次提交按钮后,竟然多次执行了相同的操作,这不仅可能导致数据混乱,还可能引发一系列连锁反应,最终让用户失望离去。这时,接口幂等性就显得尤为重要。本文将带你深入了解接口幂等性这一神秘的设计原则,揭开它的面纱,学习如何通过它来提升你的系统可靠性,并确保每一次请求都如你所愿!
接口幂等性(Idempotency)指的是在网络请求或调用中,无论请求被执行多少次,结果都是相同的,不会产生副作用或额外的影响。这意味着,即使相同的操作被重复执行多次,系统的状态不会发生变化。幂等性是设计和实现API时的重要考虑因素,尤其是在分布式系统中,确保接口的幂等性能够提高系统的可靠性和一致性。
重复提交是指用户在短时间内多次发送相同的请求或提交相同的数据,导致服务器多次处理该请求,可能会引发一系列问题。例如,用户在点击“提交订单”按钮后,由于网络延迟或系统响应缓慢,再次点击了按钮,导致系统生成了多张相同的订单。
2. 页面刷新:用户提交表单后,如果页面没有正确跳转或提示,用户可能会刷新页面,导致表单数据再次提交。
3. 双击或多次点击:用户在提交按钮上双击或多次点击,可能会导致相同请求被多次发送到服务器。
按钮禁用是防止重复提交的常见且简单的方法之一,主要通过在用户点击提交按钮后,立即禁用按钮或隐藏按钮,使用户无法再次点击,从而避免多次提交相同请求。
当用户点击提交按钮时,通常会触发一个事件,例如表单提交或数据发送到服务器。在这个事件处理过程中,按钮会被禁用或隐藏,直到服务器返回响应或操作完成。这种方法可以有效避免用户在等待响应时因为误操作而多次点击按钮,导致重复提交。
disabled 属性或修改按钮的样式来实现。下面是一个简单的JavaScript示例,展示了如何在表单提交时禁用按钮:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>按钮禁用示例</title>
</head>
<body>
<form id="myForm">
<input type="text" name="username" placeholder="Enter your name" required>
<button type="submit" id="submitButton">Submit</button>
</form>
<script>
document.getElementById("myForm").addEventListener("submit", function(event) {
var submitButton = document.getElementById("submitButton");
// 禁用按钮
submitButton.disabled = true;
// 模拟表单提交过程
setTimeout(function() {
// 在这里通常是发送表单数据到服务器,完成后可以恢复按钮状态
// submitButton.disabled = false;
alert("Form submitted!");
}, 2000); // 模拟2秒的网络延迟
});
</script>
</body>
</html>addEventListener 被用来监听表单的 submit 事件。当用户点击“Submit”按钮时,表单会触发 submit 事件。submitButton.disabled = true; 禁用了提交按钮,使其在提交过程结束之前无法再次点击。setTimeout 模拟一个表单提交过程,在实际应用中,这一部分可以替换为发送 AJAX 请求或其他异步操作。submitButton.disabled = false; 来实现。优点:
缺点:
通过这些实践,可以有效减少重复提交的问题,并改善用户体验。
在 Vue 项目中实现前端拦截器,可以在每次调用后端接口时拦截请求,确保如果前一个请求未返回状态(如请求仍在处理中或失败),则不会再次调用相同的接口。可以使用 axios 拦截器来实现这一功能。以下是具体的实现步骤,以及这种方式的优点和缺点。
安装 Axios:如果你的项目还没有安装 Axios,可以通过以下命令进行安装:
npm install axios设置请求拦截器:在项目中全局配置 Axios 的请求拦截器和响应拦截器。在拦截器中,你可以检查请求是否已被处理或仍在处理中,并采取相应的操作。
阻止重复请求:在请求拦截器中,可以为每个请求生成一个唯一的标识符(如 URL 和请求参数的组合),并记录该请求的状态。在响应拦截器中,更新请求状态或移除标识符。
示例代码:
import axios from 'axios';
// 存储正在进行的请求
const pendingRequests = new Map();
// 生成唯一标识符
function generateRequestKey(config) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
}
// 添加请求拦截器
axios.interceptors.request.use(
config => {
const requestKey = generateRequestKey(config);
if (pendingRequests.has(requestKey)) {
// 如果请求已经存在,抛出错误或取消请求
return Promise.reject({ message: '重复请求' });
}
// 记录请求
pendingRequests.set(requestKey, true);
return config;
},
error => {
return Promise.reject(error);
}
);
// 添加响应拦截器
axios.interceptors.response.use(
response => {
const requestKey = generateRequestKey(response.config);
// 请求完成后移除记录
pendingRequests.delete(requestKey);
return response;
},
error => {
const requestKey = generateRequestKey(error.config || {});
// 请求失败时也移除记录
pendingRequests.delete(requestKey);
return Promise.reject(error);
}
);通过这些措施,可以在 Vue 项目中有效地防止重复请求,提升应用的稳定性和用户体验。
除了一开始我提到的基于token的实现,还有以下几种