题记

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

限流算法

漏桶(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. HashMap的底层实现原理? HashMap 和 Hashtable的异同? 负载因子值的大小,对HashMap有什么影响?

     1. HashMap的底层实现原理 HashMap的底层:数组+链表 (jdk7及之前) 数组+链表+红黑树 (jdk 8)HashMap的底层实现原理?以jdk7为例说明: HashMap map ...

  2. 一款由 .NET 官方团队开源的电子商务系统 - eShop

    项目介绍 eShop是一款由.NET官方开源的,基于.NET Aspire构建的用于参考学习的服务架构电子商务系统,旨在展示如何利用.NET框架及其相关技术栈构建一个现代化的电子商务网站.该项目采用服 ...

  3. Kubernetes 轻松管理资源

    资源管理介绍 在kubernetes中,所有的内容都抽象为资源,用户需要通过操作资源来管理kubernetes. kubernetes的本质上就是一个集群系统,用户可以在集群中部署各种服务,所谓的部署 ...

  4. nginx适配Overlay以及测试工具

    本文分享自天翼云开发者社区<nginx适配Overlay以及测试工具>,作者:pan Overlay与Underlay介绍 Overlay网络和Underlay网络是一组相对概念,Over ...

  5. 新格局,新生态!天翼云以国云智算底座赋能AI产业发展!

    近日,中国云产业联盟暨中关村云计算产业联盟(以下简称"云联盟")主办的"首届AIGC全网小程序应用创新大会暨云联盟・移动应用专业委员会成立发布会"在中关村国家自 ...

  6. Q:windows server2019由于没有远程桌面授权服务器可以提供许可证,远程回话连接已断开

    由于没有远程桌面授权服务器可以提供许可证,远程回话连接已断开,请跟服务管理员联系 原因是服务器安装了远程桌面服务RemoteApp,这个是需要授权的.但是微软官方给予了120天免授权使用,超过120天 ...

  7. IDEA打开多个项目

    IDEA默认的情况下只能打开一个项目,即使添加了一个项目也会弹出一个窗口,将添加的项目显示在新的窗口中.通过下面操作可以,使IDEA打开过个项目. 1.1 打开项目结构 1.2 添加多个项目 点击&q ...

  8. flutter-路由传值携带中文时,报错

    解决方案: 路由采用了第三方:fluro 1 Application.router.navigateTo(context, "/searchresult?word=${Uri.encodeC ...

  9. [BZOJ3159] 决战 题解

    个人感觉各方面难度高于<在美妙的数学王国中畅游>,也不知道是不是求导的关系,这题 \(luogu\) 难度评级还更低.不过感觉这题作完对 \(LCT\) 理解更顺畅了. 前四个操作简单,关 ...

  10. mybatis - [13] 分页

    题记部分 001 || limit select * from mybatis.user limit 2,5; 2代表偏移量,从结果集的第3行开始. 5代表返回的记录数 UserMapper List ...