题记

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

限流算法

漏桶(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

java 限流的更多相关文章

  1. Java限流策略

    概要 在大数据量高并发访问时,经常会出现服务或接口面对暴涨的请求而不可用的情况,甚至引发连锁反映导致整个系统崩溃.此时你需要使用的技术手段之一就是限流,当请求达到一定的并发数或速率,就进行等待.排队. ...

  2. java限流工具类

    代码 import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.ConcurrentHashM ...

  3. java限流(一): Semaphore

    Before obtaining an item each thread must acquire a permit from the semaphore, guaranteeing that an ...

  4. java 服务接口API限流 Rate Limit

    一.场景描述 很多做服务接口的人或多或少的遇到这样的场景,由于业务应用系统的负载能力有限,为了防止非预期的请求对系统压力过大而拖垮业务应用系统. 也就是面对大流量时,如何进行流量控制? 服务接口的流量 ...

  5. java高并发系列 - 第15天:JUC中的Semaphore,最简单的限流工具类,必备技能

    这是java高并发系列第15篇文章 Semaphore(信号量)为多线程协作提供了更为强大的控制方法,前面的文章中我们学了synchronized和重入锁ReentrantLock,这2种锁一次都只能 ...

  6. rate-limit 一款 java 开源渐进式分布式限流框架使用介绍

    项目简介 rate-limit 是一个为 java 设计的渐进式限流工具. 目的是为了深入学习和使用限流,后续将会持续迭代. 特性 渐进式实现 支持独立于 spring 使用 支持整合 spring ...

  7. Java 对IP请求进行限流.

    高并发系统下, 有三把利器 缓存 降级 限流. 缓存: 将常用数据缓存起来, 减少数据库或者磁盘IO 降级: 保护核心系统, 降低非核心业务请求响应 限流: 在某一个时间窗口内对请求进行限速, 保护系 ...

  8. java实现限流

    问题产生,当调用一个接口很频繁的时候,比如每秒调用一个接口100次.业务提现在抢购等.这时我们的服务器处理不过来就会拒绝服务,宕机等等...显然这不是我们需要的. 因此产生了限流这个.限流是什么呢,就 ...

  9. Java分布式IP限流和防止恶意IP攻击方案

    前言 限流是分布式系统设计中经常提到的概念,在某些要求不严格的场景下,使用Guava RateLimiter就可以满足.但是Guava RateLimiter只能应用于单进程,多进程间协同控制便无能为 ...

  10. 关于java业务限流组件的应用推广

    可参考的链接如下: 限流算法对比.网关限流实践总结(https://segmentfault.com/a/1190000020745218) 高并发下常见的限流算法(https://www.jians ...

随机推荐

  1. thewall靶机

    includes.php 内有文件读取漏洞 一开始是想着直接用为协议写入一句话木马但是后来发现不行 因为他的文件读取方式长这样 点击查看代码 <?php include ('/var/www/h ...

  2. solon-flow 你好世界!

    solon-flow 是一个基础级的流处理引擎(可用于业务规则.决策处理.计算编排.流程审批等......).提供有 "开放式" 驱动定制支持,像 jdbc 有 mysql 或 p ...

  3. auto-wing将AI应用于自动化项目

    GitHub: https://github.com/SeldomQA/auto-wing 背景:我们之前介绍过 midsence.js AI自动化测试辅助工具,midsence.js的思路我个人是比 ...

  4. 绝了,一招解决DeepSeek 提示“服务器繁忙,请稍后再试” 卡顿问题!(保姆级教程)

    大家好,我是狂师. 现在 AI 圈里讨论最多的话题就是:"国产之光DeepSeek了". 但用过的人也知道,是真的卡.动不动就提示:"服务器繁忙,请稍后再试" ...

  5. [BZOJ2741][FOTILE模拟赛] L 题解

    相当好的题目,虽然和我前几天出的题重了qwq. \(lmx\) 是我们的红太阳,没有他我们就会死!!! 暴力枚举一个端点,然后用可持久化 \(01\ Trie\) 或者离线 \(Trie\)(当然这题 ...

  6. [BZOJ3569] DZY Loves Chinese II 题解

    考虑不联通的情况.图不好做,就造一棵生成树出来,由于是无向图,所以只有树边和返祖边. 发现在一条树边断开后,生成树会分成两个连通块,由覆盖这条树边的返祖边链接,只有这些返祖边也全部断开,原图才会不联通 ...

  7. [SDOI2015] 星际战争 题解

    假如将所有激光武器放在一边,所有机器人放在一边,激光武器向它可以伤害的机器人连边,再加超级源/汇点,这就是一个网络流问题. 考虑激光武器向机器人连的边容量无限,而机器人向超级汇点连的边容量为机器人的装 ...

  8. vue element-ui resetForm()表单重置的问题

  9. [Windows] 联发科秒开bl一键版(mtk)

    声明 不是所有的联发科都可 天机 8000 8100 9000等不行 已知 天机820 天机1000 mtk G90t 天机800 可以 其余自己测试 除了新款均可 第一步 下载软件 (是个压缩包需要 ...

  10. 一文搞懂 APP 算法备案

    今天来给大家好好科普一下超重要的 APP 算法备案,这可是和我们日常使用 APP 以及 APP 运营都息息相关的知识点哦! 什么是算法备案 简单来讲,算法备案就相当于 APP 运营者要把自家 APP ...