基於Redis接口限流策略

Reids限流

隨着業務不增加我們面對高併發場景機會也越來越多,保護我們服務器方式有很多,限流就是其中一種,小編這裏介紹並實現三種方式限流策略:暴力限流、滑動窗口限流、令牌桶限流。

暴力限流

暴力限流依據redis中key,只存儲訪問人數並設置一個過期時間,當key值大於規定人數則限流。有一個弊端:生成key時候訪問人數很少,當key還有一秒即將過期,現在來了很多請求,結果key過期了,又產生一個key值從0開始,這時服務器承擔兩倍的壓力。而下面滑動窗口可以解決這個問題。

自定義註解加AOP方式

通過方法或類上註解攔截請求方法是否需要限流(方法註解優先類上註解),如果有規定時間內大於配置請求數量就拋出異常。
因爲我們機器是集羣狀態,所以必須用redis做限流,可以避免併發等問題。

代碼
  • 自定義註解:AccessLimit
/**
 * @Author: LailaiMonkey
 * @Description:
 * @Date:Created in 2021-01-13 14:41
 * @Modifi ed By:
 */
@Documented
@Target({
   
   ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
   
   
    //標識 指定time時間段內的訪問次數限制
    int limit() default 5;

    //標識 時間段秒
    int time() default 5;

    //提示信息
    String message() default "太火爆了";
}
  • AOP實現代碼
/**
 * @Author: LailaiMonkey
 * @Description:
 * @Date:Created in 2021-01-13 14:43
 * @Modified By:
 */
@Component
@Aspect
public class RateLimitAop {
   
   

    private final static String RATE_LIMIT_KEY = "LIMIT:";

    @Autowired
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.monkey.redisLimit.AccessLimit)" +
            "||@within(com.monkey.redisLimit.AccessLimit)")
    public void serviceLimit() {
   
   

    }

    @Around("serviceLimit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
   
   
        Class<?> targetCls = joinPoint.getTarget().getClass();
        //獲取方法簽名(通過此簽名獲取目標方法信息)
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Method targetMethod =
                targetCls.getDeclaredMethod(
                        ms.getName(),
                        ms.getParameterTypes());

        //獲得路徑URI
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String requestURI = request.getRequestURI();

        //獲得方法註解
        AccessLimit accessLimit = targetMethod.getAnnotation(AccessLimit.class);
        if (accessLimit == null) {
   
   
            accessLimit = targetCls.getAnnotation(AccessLimit.class);
        }

        //是否進行限流
        boolean pass = redisLimit(RATE_LIMIT_KEY + requestURI, accessLimit.limit(), accessLimit.time());
        if (pass) {
   
   
            throw new RuntimeException(accessLimit.message());
        }

        return joinPoint.proceed();
    }

    /**
     * 是否進行限流,true:限流,false:不限流
     *
     * @param key
     * @param limit
     * @param time
     * @return
     */
    private boolean redisLimit(String key, int limit, int time) {
   
   
        //設置lua腳本
        DefaultRedisScript<Integer> rs = new DefaultRedisScript<>(buildSlideWindowLimitLuaScript(), Integer.class);

        //執行Lua腳本
        int count = (int) redisTemplate.execute(rs, Collections.singletonList(key), time);
        return count > limit;
    }

    /**
     * lua限流腳本
     * ARGV[1] 過期時間
     */
    private String buildSlideWindowLimitLuaScript() {
   
   

        StringBuilder sb = new StringBuilder();

        sb.append("local c");
        //沒有值並放入該請求數:1
        sb.append("\n c = redis.call('GET', KEYS[1])");

        //沒有值則返回1
        sb.append("\n if tonumber(c) == nil then");
        sb.append("\n redis.call('SET', KEYS[1], 1)");
        sb.append("\n redis.call('EXPIRE', KEYS[1], ARGV[1])");
        sb.append("\n return '1' ");
        //有值沒則添加1
        sb.append("\n else");
        sb.append("\n redis.call('INCR', KEYS[1])");
        sb.append("\n c = redis.call('GET', KEYS[1])");
        sb.append("\n return c ");
        sb.append("\n end");
        return sb.toString();
    }

}
  • 接口類方法
@AccessLimit
@RestController
public class TblDeptController {
   
   

	//如果類上有該註解,方法註解可以省略
	//類上註解和方法註解相沖突,方法註解優先級大於類上註解
	@AccessLimit
    @GetMapping("/AccessLimit")
    public void AccessLimit() {
   
   

    }
}

滑動窗口限流

滑動窗口算法是在給定窗口大小情況下計算結果操作。

基於redis滑動窗口限流我們可以用zset數組,當每一次請求進來的時候,我們可以給指定key值 生成一個value和score。value保持唯一,可以用UUID生成。score可以用當前時間戳表示。因爲zset數據結構也提供了range方法可以讓我們統計2個時間間隔中有多少個請求。

自定義註解加AOP方式

通過方法或類上註解攔截請求方法是否需要限流(方法註解優先類上註解),如果有規定時間內大於配置請求數量就拋出異常。
因爲我們機器是集羣狀態,所以必須用redis做限流,可以避免併發等問題。

代碼
  • 自定義註解代碼:同上

  • AOP實現代碼

/**
 * @Author: LailaiMonkey
 * @Description:
 * @Date:Created in 2021-01-13 14:43
 * @Modified By:
 */
@Component
@Aspect
public class RateLimitAop {
   
   

    private final static String RATE_LIMIT_KEY = "LIMIT:";

    @Autowired
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.monkey.redisLimit.AccessLimit)" +
            "||@within(com.monkey.redisLimit.AccessLimit)")
    public void serviceLimit() {
   
   

    }

    @Around("serviceLimit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
   
   
        Class<?> targetCls = joinPoint.getTarget().getClass();
        //獲取方法簽名(通過此簽名獲取目標方法信息)
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Method targetMethod =
                targetCls.getDeclaredMethod(
                        ms.getName(),
                        ms.getParameterTypes());

        //獲得路徑URI
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String requestURI = request.getRequestURI();

        //獲得方法註解
        AccessLimit accessLimit = targetMethod.getAnnotation(AccessLimit.class);
        if (accessLimit == null) {
   
   
            accessLimit = targetCls.getAnnotation(AccessLimit.class);
        }

        //是否進行限流
        boolean pass = redisLimit(RATE_LIMIT_KEY + requestURI, accessLimit.limit(), accessLimit.time());
        if (pass) {
   
   
            throw new RuntimeException(accessLimit.message());
        }

        return joinPoint.proceed();
    }

    /**
     * 是否進行限流,true:限流,false:不限流
     *
     * @param key
     * @param limit
     * @param time
     * @return
     */
    private boolean redisLimit(String key, int limit, int time) {
   
   
        long timeMillis = System.currentTimeMillis();
        String uuid = UUID.randomUUID().toString();
        RedisScript<Number> redisScript = new DefaultRedisScript<>(buildSlideWindowLimitLuaScript(), Number.class);

        //獲得兩時間週期內請求個數
        Number count = (Number) redisTemplate.execute(redisScript, Collections.singletonList(key), uuid, (double) timeMillis, timeMillis - time * 1000, limit);
        return count != null && count.longValue() > limit;
    }

    /**
     * lua限流腳本
     * ARGV[1] uuid
     * ARGV[2] 當前時間戳
     * ARGV[3] 當前時間戳-窗口時間
     * ARGV[4] 限制的大小
     */
    private String buildSlideWindowLimitLuaScript() {
   
   

        StringBuilder sb = new StringBuilder();

        //定義c
        sb.append("local c");
        // 刪除無用數據
        sb.append("\nredis.call('zremrangebyscore',KEYS[1],0,ARGV[3])");
        //獲取redis中的值
        sb.append("\nc = redis.call('zcard',KEYS[1])");

        //如果調用超過最大值,直接返回
        sb.append("\nif c and tonumber(c) > tonumber(ARGV[4]) then");
        sb.append("\n return c;");
        sb.append("\nend");

        //添加請求
        sb.append("\nredis.call('zadd',KEYS[1],ARGV[2],ARGV[1])");
        //獲取數量數量
        sb.append("\nreturn c + 1");

        return sb.toString();
    }
    
}
  • 接口類方法:同上

令牌桶限流

每隔一段時間系統往桶中放一定數量令牌,如果令牌已經滿了停止放入。用戶訪問接口先獲取令牌,獲得令牌可以訪問,否則拒絕訪問。

在這裏插入圖片描述

代碼
  • 自定義註解代碼:同上

這時註解中參數已經不起作用了,所有參數由定時器控制。

  • 定時器代碼
	@Autowired
    private RedisTemplate redisTemplate;

	//每秒放一個令牌
    @Scheduled(fixedDelay = 1000, initialDelay = 1000)
    public void setIntervalTimeTask() {
   
   
        Long size = redisTemplate.opsForList().size("LIMIT:/limit");
        if (size < 5) {
   
   
            redisTemplate.opsForList().rightPush("LIMIT:/limit", UUID.randomUUID().toString());
        }
    }
  • AOP實現代碼
/**
 * @Author: LailaiMonkey
 * @Description:
 * @Date:Created in 2021-01-13 14:43
 * @Modified By:
 */
@Component
@Aspect
public class RateLimitAop {
   
   

    private final static String RATE_LIMIT_KEY = "LIMIT:";

    @Autowired
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.monkey.redisLimit.AccessLimit)" +
            "||@within(com.monkey.redisLimit.AccessLimit)")
    public void serviceLimit() {
   
   

    }

    @Around("serviceLimit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
   
   
        Class<?> targetCls = joinPoint.getTarget().getClass();
        //獲取方法簽名(通過此簽名獲取目標方法信息)
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        Method targetMethod =
                targetCls.getDeclaredMethod(
                        ms.getName(),
                        ms.getParameterTypes());

        //獲得路徑URI
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String requestURI = request.getRequestURI();

        //獲得方法註解
        AccessLimit accessLimit = targetMethod.getAnnotation(AccessLimit.class);
        if (accessLimit == null) {
   
   
            accessLimit = targetCls.getAnnotation(AccessLimit.class);
        }

        //是否進行限流
        boolean pass = redisLimit(RATE_LIMIT_KEY + requestURI, accessLimit.limit(), accessLimit.time());
        if (pass) {
   
   
            throw new RuntimeException(accessLimit.message());
        }

        return joinPoint.proceed();
    }

    /**
     * 是否進行限流,true:限流,false:不限流
     *
     * @param key
     * @param limit
     * @param time
     * @return
     */
    private boolean redisLimit(String key, int limit, int time) {
   
   
        Object result = redisTemplate.opsForList().leftPop(key);
        return result == null;
    }
    
}
  • 接口類方法:同上
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章