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;
}
}
- 接口類方法:同上