通过AOP对接口进行并发控制(限流同理)

Spring的AOP机制大家也都是耳熟能详了,代码的写法也更是简单,无非就是定义@interface 注解。通过@Aspect定义切面实现类,@Pointcut定位上面的@interface 注解,再通过@Before、@After、@Around注解分别实现 之前、之后、整体环绕的业务代码。不过大多数场景@Around更实用,可以掌控切点前后的流程,更好的实现所需业务。

下面写一个使用注解实现方法的并发控制(若要实现滑块限流,水滴限流等,其实代码流程差不多,算法不一样而已)


自定义注解 @AvoidRepeatInvoke

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface AvoidRepeatInvoke {
    String prefix();
}

具体实现

@Component
@Aspect
@Order(20)
public class AvoidRepeatInvokeAdvice {
    /**
     * Redis中存放微服务调用成功标记(footPrint)的hashCode的集合名称,一定时间后自动失效;
     * 微服务方法进入时判断该标记是否存在,如存在则直接跳出,不再执行;
     * 如果不存在则执行,执行成功后放入该标记.
     */
    private static final String MSI_FP_PREFIX = "msi_fp_";
    /**
     * Redis中存放微服务调用成功的结果,有效期随footPrint.
     */
    private static final String MSI_FP_VAL_PREFIX = "msi_fpval_";
    /**
     * Redis中存放微服务处理锁的hashCode的集合名称,一定时间后自动失效;
     * 微服务方法进入时判断调用成功标记不存在之后,表示可以执行,则立即在redis中存入一个lock;
     * 无论最终执行成功与否,都需要释放该lock.
     */
    private static final String MSI_LOCK_PREFIX = "msi_lock_";
    @DubboReference(version = "0.1")
    private KVCacher kvCacher;

    /**
     * 服务调用足迹的过期时间,单位秒
     */
    @Value("${common.service.invoke.footprint.timeout}")
    private String footprintTimeout;

    /**
     * 服务调用足迹的过期时间,单位秒
     */
    @Value("${common.service.invoke.lock.timeout}")
    private String lockTimeout;

    /**
     * 定义PointCut
     */
    @Pointcut("@annotation(com.xxx.annotation.AvoidRepeatInvoke)")
    private void aspectjMethod() {
    }

    @Before(value = "aspectjMethod()")
    public void beforeAdvice(JoinPoint joinPoint) {
        //DO Nothing
    }

    @After(value = "aspectjMethod()")
    public void afterAdvice(JoinPoint joinPoint) {
        //DO Nothing
    }

    /**
     * 环绕实际业务方法前后的处理(与before + after类似)
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around(value = "aspectjMethod()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        String methodName = pjp.getSignature().getName();
        Class<?> targetClass = pjp.getTarget().getClass();
        Class<?>[] paraTypes = ((MethodSignature) pjp.getSignature()).getParameterTypes();

        Method method = targetClass.getMethod(methodName, paraTypes);
        Type genericReturnType = method.getGenericReturnType();
        Object[] args = pjp.getArgs();
        LOGGER.info("拦截幂等性方法:{},参数列表:{}", methodName, pjp.getArgs());

        AvoidRepeatInvoke annotation = method.getAnnotation(AvoidRepeatInvoke.class);
        String prefix = annotation.prefix();

        String footprint = this.getMsiFootprintKey(prefix, args, method.getName());
        String fpVal = this.getMsiFootprintValue(prefix, args, method.getName());
        String lock = this.getMsiLockKey(prefix, args, method.getName());

        if (kvCacher.exist(lock)) {
            //判断是否存在并发锁
            LOGGER.info("遇到并发锁[{}],拒绝执行.", lock);
            return null; //并发锁出现概率很小,直接返回null
        } else {
            //放入并发锁
            LOGGER.debug("新建并发锁[{}]", lock);
            kvCacher.set(lock, "1", Long.parseLong(lockTimeout), TimeUnit.SECONDS);
        }

        try {
            if (kvCacher.exist(footprint)) {
                //判断redis是否存在调用过的足迹,如有,直接返回上一次的结果
                LOGGER.warn("存在重复锁[{}]", footprint);
                String cacheResult = kvCacher.get(fpVal);
                return JSON.parseObject(cacheResult, new TypeReference4Reflect(genericReturnType).getType());
            }

            //真正的业务逻辑调用
            Object retVal = pjp.proceed(pjp.getArgs());

            //成功调用后放入重复锁和运行结果,防止一定时间内的重复调用
            kvCacher.set(footprint, "1", Long.parseLong(footprintTimeout), TimeUnit.SECONDS);
            kvCacher.set(fpVal, JSON.toJSONString(retVal), Long.parseLong(footprintTimeout), TimeUnit.SECONDS);
            LOGGER.debug("新建重复锁[{}],{}秒内不可以重复调用", footprint, footprintTimeout);

            return retVal;
        } finally {
            //释放并发锁
            LOGGER.debug("释放并发锁[{}]", lock);
            kvCacher.del(lock);
        }
    }

大概流程就是通过redis进行累计,计算是否并发。若是限流,则直接堆redis进行累加,给个最大值,超过则返回请求失败即可。

注解使用

    @ApiOperation(value = "查询")
    @GetMapping(value = "/getSingle/{aa}")
    @AvoidRepeatInvoke(prefix = "query_profix")
    public String getSingle(@PathVariable String aa){
        return "helloWorld";
    }

学习文章:https://mp.weixin.qq.com/s/kyFAWH3mVNJvurQDt4vchA
https://cloud.tencent.com/developer/article/1408819

令牌桶

import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class RouteFilter extends ZuulFilter {
    // 定义一个令牌桶,每秒产生2个令牌,即每秒最多处理2个请求
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return -5;
    }

    @Override
    public Object run() throws ZuulException {
        log.info("放行");
        return null;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        if(!RATE_LIMITER.tryAcquire()) {
            log.warn("访问量超载");
            // 指定当前请求未通过过滤
            context.setSendZuulResponse(false);
            // 向客户端返回响应码429,请求数量过多
            context.setResponseStatusCode(429);
            return false;
        }
        return true;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值