单机限流和分布式应用限流
目录
单机限流
1. 令牌桶算法
2. 漏桶算法
3. 计数器限流算法
4.漏桶和令牌桶的比较
分布式限流
单机限流
在大数据量高并发访问时,经常会出现服务或接口面对暴涨的请求而不可用的情况,甚至引发连锁反映导致整个系统崩溃。此时你需要使用的技术手段之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。在限流时,常见的两种算法是漏桶和令牌桶算法算法。
单机限流算法主要有:令牌桶(Token Bucket)、漏桶(leaky bucket)和计数器算法是最常用的三种限流的算法。
1. 令牌桶算法
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 当桶满时,新添加的令牌被丢弃或拒绝。
public class RateLimiterDemo {private static RateLimiter limiter = RateLimiter.create(5);public static void exec() {limiter.acquire(1);try {// 处理核心逻辑TimeUnit.SECONDS.sleep(1);System.out.println("--" + System.currentTimeMillis() / 1000);} catch (InterruptedException e) {e.printStackTrace();}}
}
Guava RateLimiter 提供了令牌桶算法可用于平滑突发限流策略。
该示例为每秒中产生5个令牌,每200毫秒会产生一个令牌。
limiter.acquire() 表示消费一个令牌。当桶中有足够的令牌时,则直接返回0,否则阻塞,直到有可用的令牌数才返回,返回的值为阻塞的时间。
2. 漏桶算法
它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量,数据可以以任意速度流入到漏桶中。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。 漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶为空,则不需要流出水滴,如果漏桶(包缓存)溢出,那么水滴会被溢出丢弃。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** 漏斗限流算法** @author dadiyang* @date 2018/9/28*/
public class FunnelRateLimiter {private Map<String, Funnel> funnelMap = new ConcurrentHashMap<>();public static void main(String[] args) throws InterruptedException {FunnelRateLimiter limiter = new FunnelRateLimiter();int testAccessCount = 30;int capacity = 5;int allowQuota = 5;int perSecond = 30;int allowCount = 0;int denyCount = 0;for (int i = 0; i < testAccessCount; i++) {boolean isAllow = limiter.isActionAllowed("dadiyang", "doSomething", 5, 5, 30);if (isAllow) {allowCount++;} else {denyCount++;}System.out.println("访问权限:" + isAllow);Thread.sleep(1000);}System.out.println("报告:");System.out.println("漏斗容量:" + capacity);System.out.println("漏斗流动速率:" + allowQuota + "次/" + perSecond + "秒");System.out.println("测试次数=" + testAccessCount);System.out.println("允许次数=" + allowCount);System.out.println("拒绝次数=" + denyCount);}/*** 根据给定的漏斗参数检查是否允许访问** @param username 用户名* @param action 操作* @param capacity 漏斗容量* @param allowQuota 每单个单位时间允许的流量* @param perSecond 单位时间(秒)* @return 是否允许访问*/public boolean isActionAllowed(String username, String action, int capacity, int allowQuota, int perSecond) {String key = "funnel:" + action + ":" + username;if (!funnelMap.containsKey(key)) {funnelMap.put(key, new Funnel(capacity, allowQuota, perSecond));}Funnel funnel = funnelMap.get(key);return funnel.watering(1);}private static class Funnel {private int capacity;private float leakingRate;private int leftQuota;private long leakingTs;public Funnel(int capacity, int count, int perSecond) {this.capacity = capacity;// 因为计算使用毫秒为单位的perSecond *= 1000;this.leakingRate = (float) count / perSecond;}/*** 根据上次水流动的时间,腾出已流出的空间*/private void makeSpace() {long now = System.currentTimeMillis();long time = now - leakingTs;int leaked = (int) (time * leakingRate);if (leaked < 1) {return;}leftQuota += leaked;// 如果剩余大于容量,则剩余等于容量if (leftQuota > capacity) {leftQuota = capacity;}leakingTs = now;}/*** 漏斗漏水** @param quota 流量* @return 是否有足够的水可以流出(是否允许访问)*/public boolean watering(int quota) {makeSpace();int left = leftQuota - quota;if (left >= 0) {leftQuota = left;return true;}return false;}}
}
3. 计数器限流算法
数器限流算法也是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。
public class CountRateLimiterDemo {private static Semaphore semphore = new Semaphore(5);public static void exec() {if(semphore.getQueueLength()>100){System.out.println("当前等待排队的任务数大于100,请稍候再试...");}try {semphore.acquire();// 处理核心逻辑TimeUnit.SECONDS.sleep(1);System.out.println("--" + System.currentTimeMillis() / 1000);} catch (InterruptedException e) {e.printStackTrace();} finally {semphore.release();}}
}
使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。
4.漏桶和令牌桶的比较
令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。更多算法相关:算法聚合
分布式限流
实现原理其实很简单。既然要达到分布式全局限流的效果,那自然需要一个第三方组件来记录请求的次数。
其中 Redis 就非常适合这样的场景。
- 每次请求时将方法名进行md5加密后作为Key 写入到 Redis 中,超时时间设置为 2 秒,Redis 将该 Key 的值进行自增。
- 当达到阈值时返回错误。
- 写入 Redis 的操作用 Lua 脚本来完成,利用 Redis 的单线程机制可以保证每个 Redis 请求的原子性
Lua脚本准备
local val = redis.call('incr', KEYS[1])
local ttl = redis.call('ttl', KEYS[1])redis.log(redis.LOG_NOTICE, "incr "..KEYS[1].." "..val);
if val == 1 thenredis.call('expire', KEYS[1], tonumber(ARGV[1]))
elseif ttl == -1 thenredis.call('expire', KEYS[1], tonumber(ARGV[1]))end
endif val > tonumber(ARGV[2]) thenreturn 0
endreturn 1
RateLimiter.java
@Component
public class RateLimiter {@Autowiredprivate RedisClient redisClient;@Value("${redis.limit.expire}")private int expire;@Value("${redis.limit.request.count}")private int reqCount;@Value("${redis.limit.script.name}")private String scriptName;public Long limit(String key) {return redisClient.eval(key, scriptName, 1, expire, reqCount);}
}
RateLimitAspect.java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import com.lzhsite.core.exception.OverLimitException;
import com.lzhsite.core.utils.MD5Util;
import com.lzhsite.technology.redis.limit.RateLimiter;@Aspect
@Component
public class RateLimitAspect {@Autowiredprivate RateLimiter rateLimiter;@Before("@annotation(com.lzhsite.technology.redis.limit.RateLimit)")public void before(JoinPoint pjp) throws Throwable {Signature sig = pjp.getSignature();if (!(sig instanceof MethodSignature)) {throw new IllegalArgumentException("该注解只能用在方法上");}MethodSignature msig = (MethodSignature) sig;String methodName = pjp.getTarget().getClass().getName() + "." + msig.getName();String limitKey = MD5Util.md5(methodName);if (rateLimiter.limit(limitKey) != 1){throw new OverLimitException("触发限流控制");}}
}
RateLimit.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {String value() default "";
}
@RestController
public class ShopOrderController {private static final Logger LOGGER = LoggerFactory.getLogger(ShopOrderController.class);@Autowiredprivate ShopOrderService shopOrderService;@RequestMapping("/seckill")@RateLimitpublic String index(Long stockId) {try{if (shopOrderService.createOrder(stockId)){LOGGER.info(ConstantUtil.SNATCH_DEAL_SUCCESS);return ConstantUtil.SNATCH_DEAL_SUCCESS;}} catch (Exception e){LOGGER.error(e.getMessage());return e.getMessage();}return ConstantUtil.SYSTEM_EXCEPTION;}
}
当然这只是利用 Redis 做了一个粗暴的计数器,如果想实现类似于上文中的令牌桶算法可以基于 Lua 自行实现。
完整代码
https://gitee.com/lzhcode/maven-parent/blob/78734ac309aba8f5499e0dd2eefc45c41baf0ebe/lzh-technology/src/main/java/com/lzhsite/aop/RateLimitAspect.java
参考文章
https://blog.csdn.net/sunlihuo/article/details/79700225
https://blog.csdn.net/ghaohao/article/details/80361089
单机限流和分布式应用限流相关推荐
- Java并发:分布式应用限流 Redis + Lua 实践
任何限流都不是漫无目的的,也不是一个开关就可以解决的问题,常用的限流算法有:令牌桶,漏桶.在之前的文章中,也讲到过,但是那是基于单机场景来写. 之前文章:接口限流算法:漏桶算法&令牌桶算法 然 ...
- java分布式应用限流实现
分布式应用限流 实现分布式限流的思路有很多种 基于Redis的限流 Redis的setnx的操作 Redis的数据结构zset public boolean limitZset(){int curre ...
- 可视化界面 Sentinel 流控卫兵 限流 熔断 系统保护
Sentinel 流控卫兵 Sentinel分为两个部分: 核心库(Java客户端):客户端调用通用的应用 控制台(Dashboard):基于springboot开发的,打包后可以直接运行,不需要额外 ...
- Sentinel 流控(限流)
流量控制(Flow Control),原理是监控应用流量的QPS或并发线程数等指标,当达到指定阈值时对流量进行控制,避免系统被瞬时的流量高峰冲垮,保障应用高可用性. 通过流控规则来指定允许该资源通过的 ...
- 【限流01】限流算法理论篇
微服务就是将复杂的大应用拆分成小的应用,这样做的好处是各个应用之间独立开发.测试.上线,互不影响.但是服务拆分之后,带来的问题也很多,我们需要保障微服务的正常运行,就需要进行服务治理.常用手段有:鉴权 ...
- 【限流02】限流算法实战篇 - 手撸一个单机版Http接口通用限流框架
本文将从需求的背景.需求分析.框架设计.框架实现几个层面一步一步去实现一个单机版的Http接口通用限流框架. 一.限流框架分析 1.需求背景 微服务系统中,我们开发的接口可能会提供给很多不同的系统去调 ...
- 高并发解决方案(缓存、降级、限流)之限流笔记
转载自:jinnianshilongnian 高并发系统时有三把利器用来保护系统:缓存.降级和限流.缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹:而降级是当服务出问题或 ...
- 什么是限流及如何限流
概要 在大数据量高并发访问时,经常会出现服务或接口面对暴涨的请求而不可用的情况,甚至引发连锁反映导致整个系统崩溃.此时你需要使用的技术手段之一就是限流,当请求达到一定的并发数或速率,就进行等待.排队. ...
- Redis(六)——限流算法:滑动时间窗口限流和漏斗限流
本文主要总结自<redis深度历险> 限流的意义 限流一般是指在一个时间窗口内对某些操作请求的数量进行限制,比如一个论坛限制用户每秒钟只能发一个帖子,每秒钟只能回复5个帖子.限流可以保证系 ...
最新文章
- 创建和存储 cookie
- Objective-C block
- 阿里巴巴右侧6滑块VS雅虎右侧6滑块VS自定义6滑块
- http代码_一行代码就可以实现HTTP文件服务器,他为什么写了150行?
- assertion: 18 { code: 18, ok: 0.0, errmsg: auth fails }
- 50. Element removeChild() 方法
- Quartus II使用Testbench
- Macbook下ffmpeg下载失败问题解决
- 机房服务器巡视项目,年底,机房巡检不能少
- Java applet 类
- 《南方周末》今日发文揭开了这场抢票插件阻击战的内幕
- DP(Nietzsche)的hu测 T2(dp)
- python提取html中的href标签,如何使用Python从HTML获取href链接?
- oracle rac evict,OEL6.3上 Oracle RAC 上节点驱逐检查过程
- Mysql(三)Mysql索引基本原理
- 自下而上语法制导翻译过程
- ds oracle connector 连接组件,DataStage 错误集(持续更新)
- JAVA Swt初识
- php 网站计数器,php网站计数器
- 笔记本按开机键电源灯不亮