巧用SpringBoot优雅解决分布式限流

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了巧用SpringBoot优雅解决分布式限流相关的知识,希望对你有一定的参考价值。

SpringBoot?是为了简化?Spring?应用的创建、运行、调试、部署等一系列问题而诞生的产物,自动装配的特性让我们可以更好的关注业务本身而不是外部的XML配置,我们只需遵循规范,引入相关的依赖就可以轻易的搭建出一个 WEB 工程

本篇从?Spring BootRedis?应用层面来实现分布式的限流….

分布式限流

单机版中我们了解到 AtomicInteger、RateLimiter、Semaphore 这几种解决方案,但它们也仅仅是单机的解决手段,在集群环境下就透心凉了,后面又讲述了 Nginx 的限流手段,可它又属于网关层面的策略之一,并不能解决所有问题。例如供短信接口,你无法保证消费方是否会做好限流控制,所以自己在应用层实现限流还是很有必要的。

本章目标

利用?自定义注解Spring AopRedis Cache?实现分布式限流….

具体代码

很简单…

导入依赖

在?pom.xml?中添加上?starter-webstarter-aopstarter-data-redis?的依赖即可,习惯了使用?commons-lang3?和?guava?中的一些工具包…

<dependencies>
    <!-- 默认就内嵌了Tomcat 容器,如需要更换容器也极其简单-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>21.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
</dependencies>

属性配置

在?application.properites?资源文件中添加?redis?相关的配置项

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=battcn

Limit 注解

创建一个?Limit?注解,不多说注释都给各位写齐全了….

package com.battcn.limiter.annotation;

import com.battcn.limiter.LimitType;

import java.lang.annotation.*;

/**
 * 限流
 *
 * @author Levin
 * @since 2018-02-05
 */
@Target(ElementType.METHOD, ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit 

    /**
     * 资源的名字
     *
     * @return String
     */
    String name() default "";

    /**
     * 资源的key
     *
     * @return String
     */
    String key() default "";

    /**
     * Key的prefix
     *
     * @return String
     */
    String prefix() default "";

    /**
     * 给定的时间段
     * 单位秒
     *
     * @return int
     */
    int period();

    /**
     * 最多的访问限制次数
     *
     * @return int
     */
    int count();

    /**
     * 类型
     *
     * @return LimitType
     */
    LimitType limitType() default LimitType.CUSTOMER;


public enum LimitType 
    /**
     * 自定义key
     */
    CUSTOMER,
    /**
     * 根据请求者IP
     */
    IP;

RedisTemplate

默认情况下?spring-boot-data-redis?为我们提供了StringRedisTemplate?但是满足不了其它类型的转换,所以还是得自己去定义其它类型的模板….

package com.battcn.limiter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.Serializable;

/**
 * @author Levin
 * @since 2018/8/2 0002
 */
@Configuration
public class RedisLimiterHelper 

    @Bean
    public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) 
        RedisTemplate<String, Serializable> template = new RedisTemplate<>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    

Limit 拦截器(AOP)

熟悉 Redis 的朋友都知道它是线程安全的,我们利用它的特性可以实现分布式锁、分布式限流等组件,在Spring Boot整合分布式锁中讲述了分布式锁的实现,限流相比它稍微复杂一点,官方虽然没有提供相应的API,但却提供了支持 Lua 脚本的功能,我们可以通过编写 Lua 脚本实现自己的API,同时他是满足原子性的….

下面核心就是调用?execute?方法传入我们的 Lua 脚本内容,然后通过返回值判断是否超出我们预期的范围,超出则给出错误提示。

package com.battcn.limiter;

import com.battcn.limiter.annotation.Limit;
import com.google.common.collect.ImmutableList;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;

/**
 * @author Levin
 * @since 2018/2/5 0005
 */
@Aspect
@Configuration
public class LimitInterceptor 

    private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);

    private final RedisTemplate<String, Serializable> limitRedisTemplate;

    @Autowired
    public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) 
        this.limitRedisTemplate = limitRedisTemplate;
    

    @Around("execution(public * *(..)) && @annotation(com.battcn.limiter.annotation.Limit)")
    public Object interceptor(ProceedingJoinPoint pjp) 
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        LimitType limitType = limitAnnotation.limitType();
        String name = limitAnnotation.name();
        String key;
        int limitPeriod = limitAnnotation.period();
        int limitCount = limitAnnotation.count();
        switch (limitType) 
            case IP:
                key = getIpAddress();
                break;
            case CUSTOMER:
                // TODO 如果此处想根据表达式或者一些规则生成 请看 一起来学Spring Boot | 第二十三篇:轻松搞定重复提交(分布式锁)
                key = limitAnnotation.key();
                break;
            default:
                key = StringUtils.upperCase(method.getName());
        
        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
        try 
            String luaScript = buildLuaScript();
            RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
            Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
            logger.info("Access try count is  for name= and key = ", count, name, key);
            if (count != null && count.intValue() <= limitCount) 
                return pjp.proceed();
             else 
                throw new RuntimeException("You have been dragged into the blacklist");
            
         catch (Throwable e) 
            if (e instanceof RuntimeException) 
                throw new RuntimeException(e.getLocalizedMessage());
            
            throw new RuntimeException("server exception");
        
    

    /**
     * 限流 脚本
     *
     * @return lua脚本
     */
    public String buildLuaScript() 
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("\nc = redis.call(‘get‘,KEYS[1])");
        // 调用不超过最大值,则直接返回
        lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
        lua.append("\nreturn c;");
        lua.append("\nend");
        // 执行计算器自加
        lua.append("\nc = redis.call(‘incr‘,KEYS[1])");
        lua.append("\nif tonumber(c) == 1 then");
        // 从第一次调用开始限流,设置对应键值的过期
        lua.append("\nredis.call(‘expire‘,KEYS[1],ARGV[2])");
        lua.append("\nend");
        lua.append("\nreturn c;");
        return lua.toString();
    

    private static final String UNKNOWN = "unknown";

    public String getIpAddress() 
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) 
            ip = request.getHeader("Proxy-Client-IP");
        
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) 
            ip = request.getHeader("WL-Proxy-Client-IP");
        
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) 
            ip = request.getRemoteAddr();
        
        return ip;
    

控制层

在接口上添加?@Limit()?注解,如下代码会在 Redis 中生成过期时间为 100s 的 key = test 的记录,特意定义了一个?AtomicInteger?用作测试…

package com.battcn.controller;

import com.battcn.limiter.annotation.Limit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Levin
 * @since 2018/8/2 0002
 */
@RestController
public class LimiterController 

    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();

    @Limit(key = "test", period = 100, count = 10)
    @GetMapping("/test")
    public int testLimiter() 
        // 意味著 100S 内最多允許訪問10次
        return ATOMIC_INTEGER.incrementAndGet();
    

主函数

就一个普通的不能在普通的主函数类了

package com.battcn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author Levin
 */
@SpringBootApplication
public class Chapter27Application 

    public static void main(String[] args) 
        SpringApplication.run(Chapter27Application.class, args);
    

测试

完成准备事项后,启动?Chapter27Application?自行测试即可,测试手段相信大伙都不陌生了,如?浏览器postmanjunitswagger,此处基于?postman,如果你觉得自带的异常信息不够友好,那么配上巧用SpringBoot轻松搞定全局异常?可以轻松搞定…

未达设定的阀值时

技术图片 正确响应

达到设置的阀值时

技术图片 错误响应

以上是关于巧用SpringBoot优雅解决分布式限流的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot - 优雅的实现流控

SpringBoot中如何编写一个优雅的限流组件?

Springboot分布式限流实践

SpringBoot如何进行限流,老鸟们还可以这样玩!

SpringBoot 如何进行限流?老鸟们还可以这样玩!

SpringBoot 如何进行限流?老鸟们还可以这样玩!