雲計算

分佈式接口限流實現

@[toc]
## 為什麼要接口限流

  • 在我們項目開發過程中,有些接口是暴露在用戶的常用中,包括一些高危接口,如 (支付,開發票,訂單),這些接口 都是高危接口,且被用戶經常使用,在高併發的情況下,io阻塞,不可避免的出現重複提交,或者點擊頻繁的操作,所以我們就要加入限流,避免用戶多次點擊,減少我們接口的壓力,把整數據不會重複,接口壓力減小

為什麼要做分佈式

  • 在我們做項目負載均衡的時候, 分佈式,微服務架構的時候,不可避免的多個節點,這個時候我們就要考慮會被隨機分配到各個節點,如果 我們使用 令牌桶 或者 漏斗桶 算法到話,存到 本地,各個節點不會共享,所以
    我們要考慮模塊,節點間的共享

實現方式

1. 算法實現(無分佈式,單體架構,單節點)

  1. 自定義註解
package com.yxl.annotation;

import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 限流注解,
 * </p>
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {

    int NOT_LIMITED = 0;

    /**
     * qps
     */
    @AliasFor("qps") double value() default NOT_LIMITED;

    /**
     * qps
     */
    @AliasFor("value") double qps() default NOT_LIMITED;

    /**
     * 超時時長
     */
    int timeout() default 0;

    /**
     * 超時時間單位
     */
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

}
  1. AOP實現切面 + 令牌桶算法實現
package com.yxl.aspect;

import com.yxl.annotation.RateLimiter;
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.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;


/**
 * <p>
 * 限流切面
 * </p>
 *
 * @author yxl
 * @date Created in 2019/9/12 14:27
 */
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
    private static final ConcurrentMap<String, com.google.common.util.concurrent.RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.yxl.annotation.RateLimiter)")
    public void rateLimit() {

    }

    @Around("rateLimit()")
    public Object pointcut(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        // 通過 AnnotationUtils.findAnnotation 獲取 RateLimiter 註解
        RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
        if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) {
            double qps = rateLimiter.qps();
            if (RATE_LIMITER_CACHE.get(method.getName()) == null) {
                // 初始化 QPS
                RATE_LIMITER_CACHE.put(method.getName(), com.google.common.util.concurrent.RateLimiter.create(qps));
            }

            log.debug("【{}】的QPS設置為: {}", method.getName(), RATE_LIMITER_CACHE.get(method.getName()).getRate());
            // 嘗試獲取令牌
            if (RATE_LIMITER_CACHE.get(method.getName()) != null && !RATE_LIMITER_CACHE.get(method.getName()).tryAcquire(rateLimiter.timeout(), rateLimiter.timeUnit())) {
                throw new RuntimeException("手速太快了,慢點兒吧~");
            }
        }
        return point.proceed();
    }
}

使用方式

在這裡插入圖片描述

查看結果(這裡使用了自定義異常)
在這裡插入圖片描述

2. 分佈式實現

package com.yxzapp.annotation;

import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 限流注解,
 * </p>
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {

    int NOT_LIMITED = 0;

    /**
     * 類名
     * @return
     */
    String className() default "";

    /**
     * qps
     */
    @AliasFor("qps") double value() default NOT_LIMITED;

    /**
     * qps
     */
    @AliasFor("value") double qps() default NOT_LIMITED;

    /**
     * 限流時間
     */
    int timeout() default 0;

    /**
     * 超時時間單位
     */
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

}

使用 AOP + redis 實現

package com.yxzapp.aspect;


import com.yxzapp.annotation.RateLimiter;
import com.yxzapp.commons.constant.MessageConstant;
import com.yxzapp.exception.BizException;
import com.yxzapp.modules.sys.entity.SysUser;
import com.yxzapp.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;


/**
 * <p>
 * 限流切面
 * </p>
 *
 * @author yxl
 * @date  2020/6/19
 */
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {

    @Autowired
    private RedisUtils redisUtils;

    @Pointcut("@annotation(com.yxzapp.annotation.RateLimiter)")
    public void rateLimit() {

    }

    @Around("rateLimit()")
    public Object pointcut(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class aClass = signature.getClass();

        // 獲取方法上的@RateLimiter註解
        RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
    
        if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) {
            //獲取qps
            double qps = rateLimiter.qps();
            
            String key = "RateLimiter:" rateLimiter.className() + +':'+ method.getName();
            if(!redisUtils.hasKey(key)){
                redisUtils.setMillisecond(key,rateLimiter.qps(),rateLimiter.timeout());
            }else if(redisUtils.get(key) != null) {
                throw new BizException(MessageConstant.MSG_STATUS,"手速太快了,慢點兒吧~");
            }

            log.debug("【{}】的QPS設置為: {}", key, redisUtils.get(key));

        }
        return point.proceed();
    }
}

使用方式

在這裡插入圖片描述
查看結果 (這裡使用了自定義異常)
在這裡插入圖片描述

Leave a Reply

Your email address will not be published. Required fields are marked *