用Redis实现接口限流

2022-05-05 15:41:05 浏览数 (1)

为什么需要限流

在高并发环境下,为了缓解数据库,服务器的压力,往往需要对一些接口进行限制操作。比如某个接口10s内只能调用5次,需要怎么做呢?

这里我有一个思路,利用Redisincr命令,每次调用接口,原子自增 1,当自增的值大于设定的次数时,就不让调用接口(返回接口调用次数已经用完之类的)。

Lua脚本

Lua脚本的多个命令操作可以实现原子性,下面为相关的Lua脚本命令。

代码语言:javascript复制
--获取key
local key1 = KEYS[1]

--原子自增操作
local val = redis.call('incr',key1)

--返回key的剩余生存时间
local ttl = redis.call('ttl',key1)

--获取java代码中的参数
--(Long) redisTemplate.execute(getRedisScript, keyList, expireTimes, limitTimes);
local expire = ARGV[1]
local times = ARGV[2]

--日志打印
redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))

redis.log(redis.LOG_NOTICE, "incr "..key1.."  "..val);

--给指定的key设置生存时间
if val == 1 then
    redis.call('expire',key1,tonumber(expire))
else
   if ttl == -1 then
       redis.call('expire', key1, tonumber(expire))
   end
end

--超过限制次数,返回false
if val > tonumber(times) then
   return 0
end

return 1
限流注解
代码语言:javascript复制
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {

    /**
     * 限流key
     * @return
     */
    String key() default "rate:limiter";

    /**
     * 单位时间限制通过的请求数
     * @return
     */
    long limit() default 10;

    /**
     * 过期时间,单位s
     * @return
     */
    long expire() default 1;

    /**
     * 提示
     * @return
     */
    String message() default "false";
}
AOP编程
代码语言:javascript复制
@Aspect
@Component
@Slf4j
public class RateLimiterHandler {
    @Autowired
    private RedisTemplate redisTemplate;

    private DefaultRedisScript<Long> getRedisScript;

    @PostConstruct
    public void init() {
        getRedisScript = new DefaultRedisScript<>();
        getRedisScript.setResultType(Long.class);
        getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
        log.info(">>>>>>>>>>>>>>>>RateLimiterHandler[分布式限流处理器] lua脚本加载完成");
    }

    @Pointcut("@annotation(com.lvshen.demo.redis.ratelimiter.RateLimiter)")
    public void rateLimiter() {}

    @Around("rateLimiter()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        log.info(">>>>>>>>>>>>>>>>RateLimiterHandler[分布式限流处理器]开始执行限流操作");
        /*Signature signature = point.getSignature();
        if (!(signature instanceof MethodSignature)) {
            throw new IllegalArgumentException("the Annotation @RateLimiter must used on method!");
        }*/
        MethodSignature signature = (MethodSignature) point.getSignature();
        /**
         * 获取注解参数
         */
        Method method = point.getTarget().getClass().getMethod(signature.getName(), signature.getMethod().getParameterTypes());
        RateLimiter rateLimiter = method.getAnnotation(RateLimiter.class);

        String limitKey = rateLimiter.key();
        Preconditions.checkNotNull(limitKey);

        long limitTimes = rateLimiter.limit();
        long expireTimes = rateLimiter.expire();

        log.info(">>>>>>>>>>>>>RateLimiterHandler[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}", limitTimes, expireTimes);
        String message = rateLimiter.message();
        if (StringUtils.isBlank(message)) {
            message = "false";
        }

        //执行lua脚本
        List<String> keyList = Lists.newArrayList();
        keyList.add(limitKey);

        Long result = (Long) redisTemplate.execute(getRedisScript, keyList, expireTimes, limitTimes);

        StringBuffer sb = new StringBuffer();
        if ((result == 0)) {
            String msg = sb.append("超过单位时间=").append(expireTimes).append("允许的请求次数=").append(limitTimes).append("[触发限流]").toString();
            log.info(msg);
            return message;
        }
        //执行原方法
        return point.proceed();
    }
}
测试Controller
代码语言:javascript复制
@RestController
@Slf4j
public class TestLimiterController {

    private static final String MESSAGE = "{"code":"400","msg":"FAIL","desc":"触发限流"}";

    AtomicInteger atomicInteger = new AtomicInteger();

    /**
     * 10s限制请求5次
     * @param request
     * @return
     * @throws Exception
     */
    @ResponseBody
    @RequestMapping("ratelimiter")
    @RateLimiter(key = "ratedemo:1", limit = 5, expire = 10, message = MESSAGE)
    public String test(HttpServletRequest request) throws Exception {
        int count = atomicInteger.incrementAndGet();
        return "正常请求"   count;
    }

}
测试结果

postman测试

请求第一次

请求第五次

当请求第六次时,出发限流

这只是简单的限流设计,实际使用可能还需要考虑好很多方面。不过现在Redis4.0提供了一个Redis限流模块,Redis-Cell,该模块使用了漏斗算法。并提供了原子的限流指令。有兴趣的可以自行研究下。

0 人点赞