自学内容网 自学内容网

自实现切面处理重复请求

目录

一、我的设计原理

1.请求开始前,根据key查询 查到结果:报错 | 未查到结果:存入key-value-expireTime

2.请求结束后,直接删除key ,不管key是否存在,直接删除 是否删除,可配置

3.expireTime过期时间,防止一个请求卡死,会一直阻塞,超过过期时间,自动删除

4.此方案直接切的是接口请求层面。

5.过期时间需要大于业务执行时间,否则业务请求1进来还在执行中,前端未做遮罩,或者用户跳转页面后再回来做重复请求2,在业务层面上看,结果依旧是不符合预期的。

6.建议delKey = false。即使业务执行完,也不删除key,强制锁expireTime的时间。预防5的情况发生。

7.实现思路:同一个请求ip和接口,相同参数的请求,在expireTime内多次请求,只允许成功一次。

8.页面做遮罩,数据库层面的唯一索引,先查询再添加,等处理方式应该都处理下。

9.此注解只用于幂等,不用于锁,100个并发这种压测,会出现问题,在这种场景下也没有意义,实际中用户也不会出现1s或者3s内手动发送了50个或者100个重复请求,或者弱网下有100个重复请求;

10.使用  @接口幂等注解(key = "Page")

二、注解定义

三、切面类处理


一、我的设计原理

1.请求开始前,根据key查询 查到结果:报错 | 未查到结果:存入key-value-expireTime
2.请求结束后,直接删除key ,不管key是否存在,直接删除 是否删除,可配置
3.expireTime过期时间,防止一个请求卡死,会一直阻塞,超过过期时间,自动删除
4.此方案直接切的是接口请求层面。
5.过期时间需要大于业务执行时间,否则业务请求1进来还在执行中,前端未做遮罩,或者用户跳转页面后再回来做重复请求2,在业务层面上看,结果依旧是不符合预期的。
6.建议delKey = false。即使业务执行完,也不删除key,强制锁expireTime的时间。预防5的情况发生。
7.实现思路:同一个请求ip和接口,相同参数的请求,在expireTime内多次请求,只允许成功一次。
8.页面做遮罩,数据库层面的唯一索引,先查询再添加,等处理方式应该都处理下。
9.此注解只用于幂等,不用于锁,100个并发这种压测,会出现问题,在这种场景下也没有意义,实际中用户也不会出现1s或者3s内手动发送了50个或者100个重复请求,或者弱网下有100个重复请求;
10.使用  @接口幂等注解(key = "Page")

二、注解定义



/**
 * @Author: wangwenbin
 * @Date: 2024/11/04/10:18
 * @Description:
 */
@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface 接口幂等注解 {
/**
 * 幂等操作的唯一标识,暂时使用参数名,后续可换成el表达式处理
 * @return String
 */
String key() default "";

/**
 * 有效期 默认:3 有效期要大于程序执行时间,否则请求还是可能会进来
 * @return expireTime
 */
int expireTime() default 30;

/**
 * 时间单位 默认:s
 * @return TimeUnit
 */
TimeUnit timeUnit() default TimeUnit.SECONDS;

/**
 * 提示信息,可自定义
 * @return String
 */
String info() default "重复请求,请稍后重试";

/**
 * 是否在业务完成后删除key true:删除 false:不删除
 * @return boolean
 */
boolean delKey() default false;
}

三、切面类处理


/**
 * @Author: wangwenbin
 * @Date: 2024/11/04/10:21
 * @Description:
 */
@Slf4j
@Aspect
@RequiredArgsConstructor
@Component
public class 接口幂等Aspect {
//日志
private static final Logger LOGGER = LoggerFactory.getLogger(接口幂等注解.class);
//本地线程
private static final ThreadLocal<Map<String, Object>> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new);
//redis key
private static final String RMAPCACHE_KEY = "api";
//本地线程 key
private static final String KEY = "key";
//本地线程 key
private static final String DELKEY = "delKey";
@Autowired
private RedissonClient redissonClient;

@Pointcut("@annotation(接口幂等注解)")
public void pointCut() {
}

@Before("pointCut()")
public void beforePointCut(JoinPoint joinPoint) {
//在接口请求之前我需要做的事情
//1.获取注解信息
//1.1 请求上下文获取请求
ServletRequestAttributes requestAttributes =(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//1.2 获取注解信息
MethodSignature signature =(MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (!method.isAnnotationPresent(接口幂等注解.class)) {
return;
}
接口幂等注解 annotation = method.getAnnotation(接口幂等注解.class);
//2.设置key参数信息
String key = null;
//2.1 若没有配置 key,则使用 url + 参数列表作为区分
String url = request.getRequestURL().toString();
if (!StringUtils.hasLength(annotation.key())) {
String argString = Arrays.asList(joinPoint.getArgs()).toString();
key = url + argString;
}else {
Object[] args = joinPoint.getArgs();
Optional<Object> first = Arrays.stream(args)
.filter(t -> t.toString().contains(annotation.key())).findFirst();
Object o = first.get();
key = url + o;
}
System.out.println(key);
//2.2 获取参数
long expireTime = annotation.expireTime();
String info = annotation.info();
TimeUnit timeUnit = annotation.timeUnit();
boolean delKey = annotation.delKey();
//2.3 判断请求
RMapCache<String, Object> rMapCache = redissonClient.getMapCache(RMAPCACHE_KEY);
String value = LocalDateTime.now().toString().replace("T", " ");
Object v1;
//重复请求
if (null != rMapCache.get(key)) {
// had stored
throw new RuntimeException(info);
}
synchronized (this) {
v1 = rMapCache.putIfAbsent(key, value, expireTime, timeUnit);
//重复请求
//这里双重判断,因为在设置值的过程中,可能会有多个请求通过前面那个判断
if (null != v1) {
throw new RuntimeException(info);
}
else {
LOGGER.info("[接口幂等]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime,
timeUnit, LocalDateTime.now().toString());
}
}
//3.设置一些信息到本地线程,方便后面直接拿来使用
Map<String, Object> map = THREAD_CACHE.get();
map.put(KEY, key);
map.put(DELKEY, delKey);
}

@After("pointCut()")
public void afterPointCut(JoinPoint joinPoint) {
//1.验证
//这里从本地线程得到参数
Map<String, Object> map = THREAD_CACHE.get();
if (CollectionUtils.isEmpty(map)) {
return;
}

RMapCache<Object, Object> mapCache = redissonClient.getMapCache(RMAPCACHE_KEY);
if (mapCache.size() == 0) {
return;
}
//2.删除处理
String key = map.get(KEY).toString();
boolean delKey = (boolean) map.get(DELKEY);
//是否在请求执行完后删除key,默认false,让key自动过期
if (delKey) {
mapCache.fastRemove(key);
LOGGER.info("[接口幂等]:has removed key={}", key);
}
THREAD_CACHE.remove();
}

}

 


原文地址:https://blog.csdn.net/qq_59384418/article/details/143501427

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!