java 限流

题记

在高并发的分布式系统中,我们都需要考虑接口并发量突增时造成的严重后果,后端服务的高压力严重甚至会导致系统宕机。为避免这种问题,我们都会为接口添加限流、降级、熔断等能力,从而使接口更为健壮。

限流算法

漏桶(leaky bucket)、令牌桶(Token Bucket)、计数器算法是经典的三种限流算法。最常见的是漏桶和令牌桶算法算法。

漏桶算法

漏桶算法(Leaky Bucket)的主要目的是控制资源获取的速率,平滑突发的流量。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(资源池)溢出,那么资源会被丢弃。 并且通过控制获取资源的速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。

如图所示,不管接口请求流量多大,都只能以固定的速率获取资源;当接口请求速率>获取资源速率时,就会被限流处理;

漏桶算法
所以漏桶算法的核心是:控制资源的访问速度;但是对于很多应用场景来说,除了要求能够限制资源的平均获取速率外,还要求允许某种程度的突发流量。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

令牌桶算法

令牌桶算法(Token Bucket),不再控制资源的访问速度,而是通过控制可用资源的数量进行更高效的控制。令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 当桶满时,新添加的令牌被丢弃或拒绝。

令牌桶算法

RateLimiter

Google 开源工具包 Guava 提供了限流工具类 RateLimiter,该类是基于令牌桶算法实现的流量限制,使用十分方便。
RateLimiter有两种限流模式,一种为稳定模式 (SmoothBursty: 令牌生成速度恒定),一种为渐进模式 (SmoothWarmingUp: 令牌生成速度缓慢提升直到维持在一个稳定值)

public class RateLimiterDemo {
    private static RateLimiter limiter = RateLimiter.create(5);
 
    public static void exec() {
	    //acquire()方法——从RateLimiter获取一个许可,该方法会被阻塞直到获取到请求
        if (limiter.tryAcquire()) {
			System.out.println("限流通过");
			//doSomething();
		}
    }
}

限流实现

预设场景:服务器端提供一个API供第三方平台调用, 要针对每个平台app限制调用速率;

使用ratelimiter实现单机限流

1.在配置文件中配置每个平台的限流速率,key:rate

# 平台限流参数配置(appid:rate)
rateLimit: "{\"10391\":\"1.0\",\"10392\":\"2.0\"}"

2.代码中要定义一个缓存, 缓存key: 令牌桶

	//每个appkey的限流速率
	@Value("${rateLimit}")
    private String limitMap; 
	// 根据key分不同的令牌桶, 不需要自动清理
    private static LoadingCache<String, RateLimiter> caches = CacheBuilder.newBuilder()
            .maximumSize(100)
            //.expireAfterWrite(1, TimeUnit.DAYS)
            .build(new CacheLoader<String, RateLimiter>() {
                @Override
                public RateLimiter load(String key) throws Exception {
					Map<String, String> map = JSONObject.parseObject(limitMap, Map.class);
        			String rate = map.get(key);
                    // 新的key初始化 (限流每秒两个令牌响应)
                    return RateLimiter.create(Double.valueOf(rate));
                }
            });

3.业务中调用

	private void login(String key) throws ExecutionException {
        RateLimiter limiter = caches.get(key);

        if (limiter.tryAcquire()) {
            System.out.println("成功,进入业务处理");
        } else {
            System.out.println("失败,限流处理");
			//doSomething();
        }
    }

使用redis+lua实现分布式限流

相同的预设场景:服务器端提供一个API供第三方平台调用, 要针对每个平台app限制调用速率;
但是服务端为分布式系统,则单机限流已经不能达成目的,需要有中间件来存储统一的限流信息;本文中采用数据库来实现分布式限流功能;具体代码实现如下:

配置redis

maven依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

redis 配置文件

@Configuration
public class RedisLimiterHelper {

    /**
     * @param redisConnectionFactory
     * @return
     */
    @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;
    }
}

redis配置参数

  redis:
    cluster:
      nodes:
        - ip:port
    password: xxx
    timeout: 3000
    lettuce:
      pool:
        max-active: 500
        max-idle: 30
        max-wait: 3000
# 平台限流参数配置(appid:rate)
rateLimit: "{\"10391\":\"1\",\"10392\":\"2\"}"

自定义限流注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {

    /**
     * 名字
     */
    String name() default "";

    /**
     * key
     */
    String key() default "";

    /**
     * Key的前缀
     */
    String prefix() default "";

    /**
     * 给定的时间范围 单位(秒)
     */
    int period() default 1;

    /**
     * 一定时间内最多访问次数
     */
    int count() default 0;

    /**
     * 限流的类型(用户自定义key 或者 请求ip)
     */
    String limitType() default "CUSTOMER";
}

添加拦截器

@Slf4j
@Aspect
@Configuration
public class LimitInterceptor {

    @Value("${rateLimit}")
    private String limitMap;

    private static final String UNKNOWN = "unknown";

    private final RedisTemplate<String, Serializable> limitRedisTemplate;
    @Resource
    private HttpServletRequest request;

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

    /**
     * 切面
     *
     * @param pjp
     * @return java.lang.Object
     */
    @Around("@annotation(com.limiter.java.demo.aop.Limit)")
    public Object interceptor(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        String limitType = limitAnnotation.limitType();
        String name = limitAnnotation.name();
        String key;
        /**
         * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key
         */
        switch (limitType) {
            case "IP":
                key = getIpAddress();
                break;
            case "CUSTOMER":
                key = getAppKey();
                break;
            default:
                key = StringUtils.upperCase(method.getName());
        }
        int limitPeriod = limitAnnotation.period();
        //int limitCount = limitAnnotation.count();
        int limitCount = getRate(key);

        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);
            if (count != null && count.intValue() <= limitCount) {
                log.info("Access try count is {} for name={} and key = {}", count, name, key);
                return pjp.proceed();
            } else {
                //限流处理
                //告警
                //doSomething();
                log.error("系统繁忙,限流处理中,请稍后再试");
                return "系统繁忙,限流处理中,请稍后再试";
                //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");
        }
    }

    /**
     * 编写 redis 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();
    }


    /**
     * 获取id地址
     */
    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;
    }


    /**
     * 从请求参数中获取appKey
     *
     * @return
     */
    public String getAppKey() {
        JSONObject reqObject = JSONObject.parseObject(request.getParameter("content"));
        if (!reqObject.isEmpty()) {
            return reqObject.getString("key");
        }
        return null;
    }


    /**
     * 根据平台key值获取对应的限流速率
     *
     * @return
     */
    public int getRate(String key) {
        Map<String, String> map = JSONObject.parseObject(limitMap, Map.class);
        return Integer.parseInt(map.get(key));
    }
}

使用拦截器进行接口限流

@RestController
public class TestController {

    @Limit
    @RequestMapping("/limiter")
    public String testLimiter(@RequestParam("content") String content) {
        return "成功,通过限流";
    }
}

项目demo

GitHub地址:https://github.com/helloEveryoneByChenglong/java_limiter

GitBee地址:https://gitee.com/chenglonghyGitee/java_limiter.git

posted @ 2021-03-10 20:43  super_龙  阅读(452)  评论(0编辑  收藏  举报