分布式场景下接口的限流幂等防止重复提交
Posted 抓手
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式场景下接口的限流幂等防止重复提交相关的知识,希望对你有一定的参考价值。
简单实现
定义注解
import java.lang.annotation.*;
/**
* @author 向振华
* @date 2022/11/21 18:16
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limiter
/**
* 限制时间(秒)
*
* @return
*/
long limitTime() default 2L;
/**
* 限制后的错误提示信息
*
* @return
*/
String errorMessage() default "请求频繁,请稍后重试";
定义切面
import com.alibaba.fastjson.JSONObject;
import com.xzh.web.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
/**
* @author 向振华
* @date 2022/11/21 18:16
*/
@Aspect
@Component
@Slf4j
public class LimiterAspect
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Around("@annotation(com.xzh.aop.Limiter)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Limiter annotation = method.getAnnotation(Limiter.class);
if (annotation != null)
// 获取限制key
String limitKey = getKey(joinPoint);
if (limitKey != null)
log.info("limitKey ---> " + limitKey);
Boolean hasKey = redisTemplate.hasKey(limitKey);
if (Boolean.TRUE.equals(hasKey))
// 返回限制后的返回内容
return ApiResponse.fail(annotation.errorMessage());
else
// 存入限制的key
redisTemplate.opsForValue().set(limitKey, "", annotation.limitTime(), TimeUnit.SECONDS);
return joinPoint.proceed();
public String getKey(ProceedingJoinPoint joinPoint)
// 参数
StringJoiner asj = new StringJoiner(",");
Object[] args = joinPoint.getArgs();
Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
if (asj.toString().isEmpty())
return null;
// 切入点
String joinPointString = joinPoint.getSignature().toString();
// 限制key = 切入点 + 参数
return joinPointString + ":" + asj.toString();
使用
import com.xzh.web.ApiResponse;
import com.xzh.aop.Limiter;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 向振华
* @date 2021/11/21 18:03
*/
@RestController
public class TestController
@Limiter(limitTime = 10L)
@PostMapping("/test1")
public ApiResponse<Object> test1(@RequestBody Test1DTO dto)
return ApiResponse.success("成功");
@Limiter
@PostMapping("/test2")
public ApiResponse<Object> test2(Long id, String name)
return ApiResponse.success("成功");
扩展一:自定义限制key的获取方法
定义限制key获取接口
import org.aspectj.lang.ProceedingJoinPoint;
/**
* @author 向振华
* @date 2022/11/21 18:22
*/
public interface LimiterKeyGetter
/**
* 获取限制key
*
* @param joinPoint
* @return
*/
String getKey(ProceedingJoinPoint joinPoint);
定义默认的限制key获取类
限制key = 切入点 + 请求参数,需要注意请求参数的大小,避免redis key过大。
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import java.util.Arrays;
import java.util.StringJoiner;
/**
* @author 向振华
* @date 2022/11/22 13:39
*/
public class DefaultLimiterKeyGetter implements LimiterKeyGetter
@Override
public String getKey(ProceedingJoinPoint joinPoint)
// 参数
StringJoiner asj = new StringJoiner(",");
Object[] args = joinPoint.getArgs();
Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
if (asj.toString().isEmpty())
return null;
// 切入点
String joinPointString = joinPoint.getSignature().toString();
// 限制key = 切入点 + 参数
return joinPointString + ":" + asj.toString();
备用url + sessionId的限制key获取类
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @author 向振华
* @date 2022/11/22 13:39
*/
public class UrlSessionLimiterKeyGetter implements LimiterKeyGetter
@Override
public String getKey(ProceedingJoinPoint joinPoint)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
if (servletRequestAttributes == null)
return null;
HttpServletRequest request = servletRequestAttributes.getRequest();
// 限制key = url + sessionId
return request.getRequestURL() + ":" + request.getSession().getId();
备用sha1处理的限制key获取类
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import java.util.Arrays;
import java.util.StringJoiner;
/**
* @author 向振华
* @date 2022/11/22 15:38
*/
public class Sha1LimiterKeyGetter implements LimiterKeyGetter
@Override
public String getKey(ProceedingJoinPoint joinPoint)
// 参数
StringJoiner asj = new StringJoiner(",");
Object[] args = joinPoint.getArgs();
Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
if (asj.toString().isEmpty())
return null;
// 序列号
byte[] serialize = ObjectUtil.serialize(asj.toString().hashCode());
// sha1处理
String sha1 = DigestUtil.sha1Hex(serialize).toLowerCase();
// 切入点
String joinPointString = joinPoint.getSignature().toString();
// 限制key = 切入点 + sha1值
return joinPointString + ":" + sha1;
key的获取方式
// 获取限制key
String limitKey = null;
try
limitKey = annotation.keyUsing().newInstance().getKey(joinPoint);
catch (Exception ignored)
扩展二:自定义限制后的返回策略
定义返回策略枚举类
/**
* @author 向振华
* @date 2022/11/22 15:50
*/
public enum ReturnStrategy
/**
* 返回错误提示信息
*/
ERROR_MESSAGE,
/**
* 返回上次执行的结果
*/
LAST_RESULT,
LAST_RESULT策略的实现逻辑:
将执行结果和限制key一起存入redis,然后判断需要限制时,从redis取出执行结果并返回出去。
扩展三:提供重试规则
重试一定次数
在被限制时,重试n次,n次后如果依然被限制,则不再重试。
等待一定时间后重试
等待n秒后重试1次,如果依然被限制,则不再重试。
等待一定时间后重试一定次数
等待n秒后重试n次,如果依然被限制,则不再重试。
扩展后的注解
import com.xzh.aop.key.DefaultLimiterKeyGetter;
import com.xzh.aop.key.LimiterKeyGetter;
import java.lang.annotation.*;
/**
* @author 向振华
* @date 2022/11/21 18:16
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limiter
/**
* 限制时间(秒)
*
* @return
*/
long limitTime() default 2L;
/**
* 限制后的错误提示信息
*
* @return
*/
String errorMessage() default "请求频繁,请稍后重试";
/**
* 限制key获取类
*
* @return
*/
Class<? extends LimiterKeyGetter> keyUsing() default DefaultLimiterKeyGetter.class;
/**
* 限制后的返回策略
*
* @return
*/
ReturnStrategy returnStrategy() default ReturnStrategy.ERROR_MESSAGE;
以上是关于分布式场景下接口的限流幂等防止重复提交的主要内容,如果未能解决你的问题,请参考以下文章
Spring Cloud项目如何防止重复提交,防重复提交幂等校验,Redis+aop+自定义Annotation实现接口