接口防重
防重复提交
接口防重复提交是防止用户在短时间内多次点击提交按钮或重复发送相同请求导致的多次执行同一操作的问题,这对于保护数据一致性、避免资源浪费非常重要。
意义
1.数据一致性:
(1)避免重复数据:防止用户重复提交表单或请求,导致数据库中产生重复的数据记录。
(2)防止数据冲突:在并发情况下,如果同一个用户或不同用户同时提交相同的数据请求,可能会导致数据冲突或覆盖,破坏数据的一致性
2.系统安全性:
(1)防止恶意攻击:防重提交可以防止恶意用户通过快速、连续提交请求来进行“刷单”或“刷量”等攻击行为,保护系统的稳定性和安全性。
(2)保护系统资源:避免系统因处理大量重复请求而浪费计算资源、网络带宽和存储空间,提升系统性能和响应速度。
3.防止用户误操作:用户可能由于网络延迟或浏览器卡顿而多次点击提交按钮,防重提交可以避免这种误操作导致的重复提交。
4. 业务逻辑正确性:
(1)防止重复支付:在支付场景中,防止用户重复支付,避免资金扣除多次,导致退款和客服问题的增加。
(2)保证交易一致性:在电商、金融等对数据准确性要求高的业务场景中,确保每笔交易的唯一性和一致性,避免产生业务逻辑错误。
实现步骤
步骤一:定义防重提交注解 RepeatSubmit
package com.litchi.annotation;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 设置默认的防重提交方式为基于方法参数。也可以使用默认值
*/
Type limitType() default Type.PARAM;
/**
* 允许设置加锁的过期时间,默认为5秒。这意味着在第一次请求之后的5秒内,相同的请求将被视为重复并被阻止
*/
long lockTime() default 5;
//提供了一个可选的服务ID参数,通过token时用作KEY计算
String serviceId() default "";
/**
* 定义了两种防止重复提交的方式,PARAM 表示基于方法参数来防止重复,TOKEN 则可能涉及生成和验证token的机制
*/
enum Type {PARAM, TOKEN}
}
步骤二:RedissonClient 的配置和使用
该实现使用了redisson的分布式锁,所以需要引入
Maven
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.6</version>
</dependency>
配置redisson
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");//这里我使用的是本机
return Redisson.create(config);
}
}
配置三:定义切入点和环绕通知
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Aspect // 定义一个切面
@Component // 标识为Spring组件
public class NoRepeatSubmitAspect {
@Autowired
private RedissonClient redissonClient; // 注入RedissonClient,用于分布式锁
// 定义切入点,匹配所有被RepeatSubmit注解标注的方法
@Pointcut("@annotation(repeatSubmit)")
public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
}
// 定义环绕通知
@Around("pointCutNoRepeatSubmit(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
// 获取当前HTTP请求
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String serviceId = repeatSubmit.serviceId(); // 获取RepeatSubmit注解中的serviceId
String type = repeatSubmit.limitType().name(); // 获取防重提交的类型
if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
long lockTime = repeatSubmit.lockTime(); // 获取锁的超时时间
String ipAddr = request.getRemoteAddr(); // 获取客户端IP地址
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod(); // 获取目标方法
// 生成唯一的key,用于获取分布式锁
String key = generateUniqueKey(ipAddr, method, serviceId);
boolean isLocked = redissonClient.getLock(key).tryLock(lockTime, TimeUnit.MILLISECONDS); // 尝试获取锁
if (!isLocked) {
throw new RuntimeException("重复提交,请稍后再试"); // 如果获取锁失败,抛出异常
}
try {
Object result = joinPoint.proceed(); // 执行目标方法 这里的目标方法是使用了@repeatSubmit注解的方法
return result; // 返回目标方法的执行结果
} finally {
redissonClient.getLock(key).unlock(); // 释放锁 不释放的话可以在上面上锁时定义自动释放
}
}
return joinPoint.proceed(); // 默认执行目标方法
}
// 生成唯一的key,用于获取分布式锁
private String generateUniqueKey(String ipAddr, Method method, String serviceId) {
return ipAddr + ":" + method.getDeclaringClass().getName() + "." + method.getName() + ":" + serviceId;
}
}
步骤四:配置Bean交给容器管理
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RepeatAutoConfiguration {
@Bean
public RepeatSubmitAspect repeatSubmitAspect() {
return new RepeatSubmitAspect();
}
}
步骤五:测试
@Controller
@RequestMapping("/repeat")
public class repeatController {
@PostMapping("/users/save")
@RepeatSubmit(serviceId = "saveUser", limitType = RepeatSubmit.Type.PARAM, lockTime = 5)//这个为目标方法 //使用防重,@Pointcut("@annotation(repeatSubmit)") public void //pointCutNoRepeatSubmit(RepeatSubmitrepeatSubmit) {}方法的意思就是监听使用 @RepeatSubmit注解的方法然后进行切点注入
public ResponseEntity<String> saveUser() {
System.out.println("调用目标方法");
return ResponseEntity.ok("用户保存成功");
}
}
http://localhost:8091/repeat/users/save
HTTP/1.1 200
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 26 Jul 2024 03:10:14 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"code": 11111,
"message": "请勿重复提交",
"error": "您的操作太快了,请稍后重试",
"data": null
}