-
在网络卡顿时,容易出现在极短的时间内产生重复请求,或重复支付,一般我们会在数据操作时先通过数据查询是否存在,然后再进行业务逻辑操作的方式来进行避免,但是这种方式并不是原子性,很容易出现第一次请求未进行落表,第二次重复的请求就已经通过了数据库查询,可通过设置唯一索引的方式来避免,但是这样比较耗费性能的,如何把这种极端情况下的请求进行排除掉呢
-
设计流程如下
-
代码实现
- AOP拦截时需要支持spel表达式进行处理,因为很多的幂等请求都是动态的,本文中将基于Redisson进行接口幂等的实现
-
首先创建一个注解spElValue是一个字符串数组,可以支持多种方式的组合来进行过滤
package com.sw.xyz.springframework.cache.annotations; import java.lang.annotation.*; /** * 名称: IdemPoeNce * 功能: <功能详细描述> * 方法: <方法简述-方法描述> * 版本: 1.0 * 作者: sunyw * 说明: 幂等性注解,使用SpEl表达式进行解析,目的是为了解决同一时间节点的重复请求 * 时间: 2022/12/21 0021 17:58 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface IdemPoeNce { /** * 解析的数据值 */ String[] spElValue() default ""; /** * 时间,时间范围,时间秒 */ long time() default 1; }
-
对应的切面,逻辑为,请求时从对象中取spEl表达式对应的value,拼装类的全路径和方法名称+参数值作为幂等的条件值,切面中的异常处理和返回参数可以按照实际的情况做处理,文本做的方式是直接返回错误提示信息
package com.sw.xyz.springframework.cache.config; import cn.hutool.core.lang.Validator; import com.sw.xyz.springframework.bean.entity.enums.RespCodeEnums; import com.sw.xyz.springframework.bean.exceptions.BaseException; import com.sw.xyz.springframework.cache.annotations.IdemPoeNce; import com.sw.xyz.springframework.cache.redisson.RedissonBaseUtils; import com.sw.xyz.springframework.utils.spel.SpElUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit; /** * 名称: IdemPoeNceAspect * 功能: <幂等性校验注解> * 方法: <方法简述-方法描述> * 版本: 1.0 * 作者: sunyw * 说明: 在一段时间节点内防止重复点击 * 时间: 2022/12/21 0021 18:01 */ @Aspect @Component @Slf4j public class IdemPoeNceAspect { @Autowired private RedissonBaseUtils<String> baseUtils; @Autowired private SpElUtils spElUtils; @Pointcut(value = "@annotation(com.sw.xyz.springframework.cache.annotations.IdemPoeNce)") public void cut() { } @Before(value = "cut()") public void check(JoinPoint joinPoint) { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); IdemPoeNce poeNce = method.getAnnotation(IdemPoeNce.class); if (null == poeNce) { return; } String value = buildKey(poeNce, method, joinPoint.getArgs()); boolean result = baseUtils.TrySaveObject(value, value, poeNce.time(), TimeUnit.SECONDS); if (!result) { throw new BaseException(RespCodeEnums.REPEATABLE_REQUESTS.getCode(), RespCodeEnums.REPEATABLE_REQUESTS.getMessage()); } } /** * 构建幂等性的Key,由Key值加动态参数值构成 * * @param rateLimit {@link RateLimit} * @param method {@link Method} * @return String */ private String buildKey(IdemPoeNce rateLimit, Method method, Object[] args) { String value = method.getDeclaringClass().getName() + "." + method.getName(); String spElValue = spElUtils.parseSpEl(method, rateLimit.spElValue(), args); if (Validator.isNotEmpty(spElValue)) { value = value + "#" + spElValue; } return value; } }
-
Redisson工具类
package com.sw.xyz.springframework.cache.redisson; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RAtomicLong; import org.redisson.api.RBucket; import org.redisson.api.RMapCache; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * 名称: Redisson 操作 Redis * 功能: 获取操作Redis的数据对象 * Redisson 获取连接的数据对象, 可直接对Redis数据进行操作 * 方法: <方法简述-方法描述> * 版本: 1.0 * 作者: sunyw * 说明: 说明描述 * 时间: 2021/12/7 10:05 */ @Component @Slf4j public class RedissonBaseUtils<T> { private final RedissonClient redissonClient; public RedissonBaseUtils(RedissonClient redissonClient) { this.redissonClient=redissonClient; } /** * 尝试保存Object,当已经存在一个值时,返回false * * @param key 键 * @param value 值 * @param time 过期时间 * @param timeUnit 过期时间单位 * @return true success */ public boolean TrySaveObject(String key,T value,Long time,TimeUnit timeUnit) { try{ RBucket<Object> bucket=redissonClient.getBucket(key); return bucket.trySet(value,time,timeUnit); } catch (Exception e) { log.error("TrySaveObject",e); } return false; } }
-
SpEl解析的工具类
package com.sw.xyz.springframework.utils.spel; import cn.hutool.core.lang.Validator; import cn.hutool.core.util.ArrayUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MethodBasedEvaluationContext; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * 名称: SpelUtils * 功能: <功能详细描述> * 方法: <方法简述-方法描述> * 版本: 1.0 * 作者: sunyw * 说明: 说明描述 * 时间: 2022/12/2 0002 14:59 */ @Component @Slf4j public class SpElUtils { /** * 用于SpEL表达式解析. */ private final static SpelExpressionParser parser = new SpelExpressionParser(); /** * 用于获取方法参数定义名字. */ private final static DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer(); private BeanResolver beanResolver; public SpElUtils(BeanFactory beanFactory) { this.beanResolver = new BeanFactoryResolver(beanFactory); } public String parseSpEl(Method method, String[] keys, Object[] args) { StringBuilder sbu = new StringBuilder(); try { if (!ArrayUtil.isEmpty(keys)) { StandardEvaluationContext context = new MethodBasedEvaluationContext(null, method, args, nameDiscoverer); context.setBeanResolver(beanResolver); for (int i = 0; i < keys.length; i++) { String value = keys[i]; if (Validator.isNotEmpty(value)) { String parseValue = parser.parseExpression(value).getValue(context, String.class); sbu.append(parseValue); if (i < keys.length - 1) { sbu.append("."); } } } } } catch (Exception e) { log.error("spEl解析失败,错误信息[{}]", e.getMessage()); } return sbu.toString(); } }
-
实际使用,只需要在接口上添加注解,spElValue指定请求对象的参数值
-
测试:使用相同的userId在一秒内重复请求
-
正常返回