即限制流量进入

缓存,是用来增加系统吞吐量,提升访问速度提供高并发。

降级,是在系统某些服务组件不可用的时候、流量暴增、资源耗尽等情况下,暂时屏蔽掉出问题的服务,继续提供降级服务,给用户尽可能的友好提示,返回兜底数据,不会影响整体业务流程,待问题解决再重新上线服务

限流,是指在使用缓存和降级无效的场景。比如当达到阈值后限制接口调用频率,访问次数,库存个数等,在出现服务不可用之前,提前把服务降级。只服务好一部分用户。

在我们使用微信、支付宝、短信等等这些api的时候,每个接口都会有调用上的限流。

限流是对某一时间窗口内的请求数进行限制,保持系统的可用性、稳定性和安全性,防止因流量暴增而导致的系统运行缓慢或宕机,

app 统计

友盟 cnzz .....

限流算法

计数器算法

简单粗暴

比如线程池大小,数据库连接池大小、nginx连接数等都属于计数器算法。

全局或某段时间范围达到阈值则限流。

漏桶算法

削峰
缓冲
消费速度固定 因为计算性能固定
保证桶不能忙

令牌桶算法

平滑的流入速率限制,消费/秒。

可以用于对外服务接口,内部集群调用

区别

  • 令牌桶是按照固定速率从桶里拿令牌消费,如果令牌为0,则拒绝新请求
  • 漏桶是按照固定速率流出请求,流入速率不控制,当桶内请求达到阈值,新请求则被拒绝。
  • 令牌桶支持每次拿多个令牌,平均流入速率,并支持突发流入,还可以支持缓慢提升流入速度

并发限流

设置系统阈值总的qps个数

Tomcat中配置的

  • acceptCount 响应连接数
  • maxConnections 瞬时最大连接数
  • maxThreads 最大线程数

接口限流

接口总数

可以使用atomic类或者semaphore进行限流

这种方式简单粗暴。没有平滑处理。使用限制某个接口的总并发数,或限制某账号服务调用总次数。

比如某些开放平台限制试用账号。

if (atomic.incrementAndGet() > 100){
    // 拒绝
}finally{
	atomic.decrementAndGet();

}
接口时间窗口

此时可以使用Guava Cache,类似于一个ConcurrentMap,但并不完全一样。

最基础的不同是ConcurrentMap保存所有的元素知道它们被明确删除,Guava Cache可以配置自动过期

//计数器
counter;
// 限制数量
limit;
// 限制单位 1000=秒
unit;
// 获得当前时间
current = system.currentTimeMillis() / unit
//判断时间窗内是否限制访问

if (counter.get(current).incrementAndGet() > limit){
    // 拒绝
}

使用guava实现

引入包

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.1-jre</version>
</dependency>

核心代码

		LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder().
				expireAfterWrite(2, TimeUnit.SECONDS)
				.build(new CacheLoader<Long, AtomicLong>() {

					@Override
					public AtomicLong load(Long secend) throws Exception {
						// TODO Auto-generated method stub
						return new AtomicLong(0);
					}
				});
		counter.get(1l).incrementAndGet();

令牌桶实现

稳定模式(SmoothBursty:令牌生成速度恒定)

	public static void main(String[] args) {
		// RateLimiter.create(2)每秒产生的令牌数
		RateLimiter limiter = RateLimiter.create(2);
        // limiter.acquire() 阻塞的方式获取令牌
		System.out.println(limiter.acquire());;
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
	}

```RateLimiter.create(2)`` 容量和突发量,令牌桶算法允许将一段时间内没有消费的令牌暂存到令牌桶中,用来突发消费。

渐进模式(SmoothWarmingUp:令牌生成速度缓慢提升直到维持在一个稳定值)

	// 平滑限流,从冷启动速率(满的)到平均消费速率的时间间隔
		RateLimiter limiter = RateLimiter.create(2,1000l,TimeUnit.MILLISECONDS);
		System.out.println(limiter.acquire());;
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;
		
		System.out.println(limiter.acquire());;
		System.out.println(limiter.acquire());;

超时

boolean tryAcquire = limiter.tryAcquire(Duration.ofMillis(11));

在timeout时间内是否能够获得令牌,异步执行

分布式系统限流

Nginx + Lua

可以使用resty.lock保持原子特性,请求之间不会产生锁的重入

https://github.com/openresty/lua-resty-lock

使用lua_shared_dict存储数据

local locks = require "resty.lock"

local function acquire()
    local lock =locks:new("locks")
    local elapsed, err =lock:lock("limit_key") --互斥锁 保证原子特性
    local limit_counter =ngx.shared.limit_counter --计数器

    local key = "ip:" ..os.time()
    local limit = 5 --限流大小
    local current =limit_counter:get(key)

    if current ~= nil and current + 1> limit then --如果超出限流大小
       lock:unlock()
       return 0
    end
    if current == nil then
       limit_counter:set(key, 1, 1) --第一次需要设置过期时间,设置key的值为1,
过期时间为1else
        limit_counter:incr(key, 1) --第二次开始加1即可
    end
    lock:unlock()
    return 1
end
ngx.print(acquire())