本节基于Redis实现了系统的限流控制。
Lua脚本准备
local val = redis.call('incr', KEYS[1]) local ttl = redis.call('ttl', KEYS[1]) redis.log(redis.LOG_NOTICE, "incr "..KEYS[1].." "..val); if val == 1 then redis.call('expire', KEYS[1], tonumber(ARGV[1])) else if ttl == -1 then redis.call('expire', KEYS[1], tonumber(ARGV[1])) end end if val > tonumber(ARGV[2]) then return 0 end return 1
对传入的key做incr操作,如果key首次生成,设置超时时间ARGV[1];
ttl是为防止某些key在未设置超时时间并长时间已经存在的情况下做的保护的判断;
判断当前key的val是否超过限制次数ARGV[2]。
Redis客户端
public Long limit(String key) { return redisClient.eval(key, expire, reqCount); }
加载lua脚本,并对外提供Redis调用的接口。
AOP
@Before("@annotation(com.snatch.deal.shop.common.annotations.RateLimit)") public void before(JoinPoint pjp) throws Throwable { Signature sig = pjp.getSignature(); if (!(sig instanceof MethodSignature)) { throw new IllegalArgumentException("该注解只能用在方法上"); } MethodSignature msig = (MethodSignature) sig; String methodName = pjp.getTarget().getClass().getName() + "." + msig.getName(); String limitKey = Md5Utils.encrypt(methodName); if (rateLimiter.limit(limitKey) != 1){ throw new OverLimitException("触发限流控制"); } }
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimit { String value() default ""; }
利用AOP的before实现方法执行前的拦截,所有注解了RateLimit的方法都会被缓存限流。一旦触发限流,对外抛出异常。
统一异常处理
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(OverLimitException.class) @ResponseBody public String OverLimitExceptionHandler(OverLimitException ole){ return ole.getMessage(); } }
利用ControllerAdvice处理对应的异常的处理。