出现接口幂等这种问题几乎都是商城的项目,基本上也就是下单的接口会出现的问题。如何防止接口中同样的数据提交,以及如何保证消息不被重复消费,我们这边使用AOP+Redis进行实现。
注意:本篇文章仅使用于单机的场景,对于分布式、高并发场景,还是建议使用分布式锁。
解决方案:redis的set方法。
代码实现
- 自定义注解
Idempotent
/**
* @author YanChengHao
* @data 2024/08/28
* 自定义注解 作用:防止下单重复提交
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// value表示接口的唯一标识,可以为空
String value() default "";
}
其中的value表示接口的唯一标识,可以为空,下边的IdempotentAspect
中会讲到
- 定义
IdempotentAspect
的切片
/**
* @author YanChengHao
* @data 2024/08/28
* 这里主要是定义一个切片的环绕通知,在里边处理主要的接口防刷逻辑
*/
@Aspect
@Component
public class IdempotentAspect {
@Resource
private IdempotentProcessor idempotentProcessor;
private static final ThreadLocal<String> identifierHolder = new ThreadLocal<>();
@Around("@annotation(idempotent)")
public Object handleIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 获取方法参数和相关信息
Signature signature = joinPoint.getSignature();
MethodSignature methodsignature = (MethodSignature) signature;
Method method = methodsignature.getMethod();
Object[] args = joinPoint.getArgs();
// 根据方法名和参数生成唯一标识
String identifier =idempotent.value().isEmpty()
? idempotentProcessor.generateIdentifier(method.getName(), args):
idempotent.value();
// 将 identifier 存储在 ThreadLocal 中
identifierHolder.set(identifier);
// 使用幂等性处理器处理幂等性逻辑
boolean isIdempotent = idempotentProcessor.process(identifier);
if(!isIdempotent){
//如果已经存在相同的操作
throw new CommonException("the same requests 请勿重复操作");
}
return joinPoint.proceed();
}
/**
* 当方法执行完之后执行该后置通知方法,对当前存储的redis进行释放的操作
* @param idempotent
*/
@After("@annotation(idempotent)")
public void AfterIdempotent(Idempotent idempotent) {
String identifier = identifierHolder.get();
if (identifier != null) {
// 删除该缓存
idempotentProcessor.clear(identifier);
// 清除 ThreadLocal 中的 identifier
identifierHolder.remove();
}
}
}
这里主要是定义一个切片的环绕通知,在里边处理主要的接口防刷逻辑
- 幂等性处理类
IdempotentProcessor
/**
* @author YanChengHao
* @data 2024/08/28
* 接口的唯一标识变成了方法名+方法的参数
*/
public interface IdempotentProcessor {
/**
* 根据标识判断是否重复
* @param identifier 标识
* @return 是否重复
*/
boolean process(String identifier);
/**
* 删除缓存
* @param identifier 标识
* @return
*/
boolean clear(String identifier);
/**
* 生成唯一标识
* @param methodName 方法名
* @param args
* @return 唯一标识
*/
default String generateIdentifier(String methodName, Object[] args){
String argsString= Stream.of(args)
.filter(Objects::nonNull)
.map(Object::toString)
.collect(Collectors.joining());
// 获取当前人的ID
UserAuthorization user = ControllerContent.getUserAuthorization();
// 使用方法名 和 参数字符串 以及 当前登录人ID进行拼接
System.out.println("用户的信息:" + user);
return methodName + argsString + user.getUserId();
}
}
接口的唯一标识变成了方法名+方法的参数+用户唯一ID
- 幂等性处理接口
IdempotentProcessor
的实现类RedisIdempotentProcessor
/**
* @author YanChengHao
* @data 2024/08/28
*/
@Component
public class RedisIdempotentProcessor implements IdempotentProcessor{
@Resource
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean process(String identifier) {
String isIdempotent =redisTemplate.opsForValue().get(identifier);
if(!StringUtils.isEmpty(isIdempotent)){
// 如果标识已存在,则返回 false,表示幂等性已满足
return false;
} else {
// 如果标识不存在,则将标识存储到 Redis 中,并设置过期时间 5秒过期时间
redisTemplate.opsForValue().set(identifier,"true",5, TimeUnit.SECONDS);
return true;
}
}
@Override
public boolean clear(String identifier) {
Boolean delete = redisTemplate.delete(identifier);
// 当前key不存在 或被删除 为 false
return delete;
}
我这边存储时间设置的5秒,这边切面类有@After,在方法执行完之后释放缓存
好的所有的准备已经就绪,现在我们写一个测试的接口测试一下:
直接写上一个注解即可。我们还是采用jmeter进行测试