1. 流控基础概念:流量控制、为什么需要、常见算法
各位好,我是老张。今天咱们聊聊流控最基础的东西。你可能会觉得「基础」嘛,翻翻文档就懂了。但我带过不少团队,发现很多人栽跟头恰恰是在这些「基础」上。所以这一章,咱们把地基打扎实。
1.1 什么是流量控制
流量控制,说白了就是限制单位时间内进入系统的请求量。就像你进地铁站,闸机一次只放一个人,不会让所有人一窝蜂冲进去。
我习惯把流控比作水龙头:后端服务是水池,请求就是水流。水太大,池子会溢;水太小,池子又空着浪费。流控就是那个恰到好处的阀门。
核心定义:流量控制是一种通过限制请求速率、队列深度或并发数,来保护系统不被突发流量冲垮的机制。
举个例子。我几年前维护过一个电商订单系统。平时每秒几百单,双十一峰值能到每秒几万单。如果没有流控,数据库连接池瞬间被打满,整个服务就挂了。嗯,那场面,运维群里全是告警。
1.2 为什么需要流控
你想想看,一个系统能承载的容量是有上限的。CPU、内存、数据库连接、网络带宽,哪个都不是无限的。流控要解决的就是三个问题:
- 防止雪崩:一个服务挂了,上游重试,下游压力更大,最后整条链路全挂。流控能在源头掐住。
- 保证公平:不能让某个用户或某个接口把资源全占了。我见过一个案例,某个爬虫客户端把整个 API 网关打满,正常用户全被拒绝。
- 削峰填谷:流量有波峰波谷。流控可以把波峰的请求排队,在波谷慢慢消化。就像水库蓄水一样。
注意:流控不是万能的。它只能缓解瞬时压力,不能解决容量不足的根本问题。如果长期超过系统上限,该扩容还是要扩容。
1.3 常见流控算法
算法这块,我见过很多新手一上来就背概念,结果面试过了,线上出问题却不知道怎么调。咱们换个思路,先理解每种算法解决了什么问题,再记公式。
1.3.1 漏桶算法
漏桶算法,形象点说:你有一个桶,底部有个小洞。水(请求)以恒定速率从洞里漏出。不管上面倒水多快,下面出水永远是匀速的。
它的核心思想是强制限速。桶满了,新来的请求直接丢弃。
// 漏桶算法伪代码
class LeakyBucket {
capacity // 桶容量
rate // 漏水速率(请求/秒)
water // 当前水量
lastTime // 上次漏水时间
function allowRequest():
now = currentTime()
// 先漏水
leaked = (now - lastTime) * rate
water = max(0, water - leaked)
lastTime = now
if water < capacity:
water++
return true // 放行
else:
return false // 拒绝
}
我在项目中用过漏桶处理日志上报。日志量忽大忽小,但后端写入 Elasticsearch 的速率必须稳定。漏桶正好合适——不管前端怎么狂刷日志,后端始终匀速写入。
适用场景:需要严格平滑流量、保护下游系统不被突发流量冲击的场景。比如数据库写入、消息队列生产端。
1.3.2 令牌桶算法
令牌桶跟漏桶正好相反。它允许一定程度的突发。怎么做的呢?桶里放令牌,以固定速率生成。请求来了,拿一个令牌就走。桶里有令牌就能走,没有就等或者拒绝。
关键区别:令牌桶允许短时间内的突发流量。因为桶里可以攒令牌,攒多了就能一次性放行多个请求。
// 令牌桶算法伪代码
class TokenBucket {
capacity // 桶容量(最大令牌数)
rate // 令牌生成速率(个/秒)
tokens // 当前令牌数
lastTime // 上次生成令牌时间
function allowRequest():
now = currentTime()
// 先补充令牌
newTokens = (now - lastTime) * rate
tokens = min(capacity, tokens + newTokens)
lastTime = now
if tokens >= 1:
tokens--
return true // 放行
else:
return false // 拒绝
}
我个人习惯用令牌桶做 API 网关限流。比如某个接口限制每秒 100 次,但允许瞬间 200 次的突发。令牌桶的 capacity 设成 200,rate 设成 100,就能实现「平均 100,峰值 200」的效果。
漏桶 vs 令牌桶:漏桶强制匀速,适合保护下游;令牌桶允许突发,适合应对用户行为的不确定性。选哪个,取决于你的业务容忍度。
1.3.3 滑动窗口算法
滑动窗口,你可以理解成一个会移动的时间段。比如限制 1 分钟内最多 100 个请求。传统做法是固定每分钟重置一次计数器。但问题来了:如果用户在 59 秒时发了 100 个请求,下一秒又发了 100 个,实际上 2 秒内就发了 200 个,突破了限制。
滑动窗口解决了这个问题。它把时间切成小格子(比如 1 秒一格),窗口随着时间平滑移动。每个格子独立计数,窗口内的总请求数就是所有格子之和。
// 滑动窗口算法伪代码
class SlidingWindow {
windowSize // 窗口大小(秒)
bucketCount // 格子数
buckets // 格子数组,每个格子记录请求数
currentTime // 当前时间
function allowRequest():
now = currentTime()
// 找到当前格子
bucketIndex = (now / windowSize) % bucketCount
// 如果格子过期,重置
if buckets[bucketIndex].time != now:
buckets[bucketIndex].reset()
buckets[bucketIndex].count++
// 计算窗口内总请求数
total = sum(buckets)
if total <= limit:
return true
else:
return false
}
我曾经踩过一个坑。用固定窗口限流,结果某个用户卡着整点边界刷接口,把我们的优惠券库存刷走了大半。后来换成滑动窗口,这种边界攻击就防住了。
注意:滑动窗口的格子数越多,精度越高,但内存消耗也越大。一般 10~100 个格子就够用了。别为了追求完美把内存打满。
1.4 三种算法对比
我把它们放在一张表里,方便你对照选型:
| 算法 | 核心特点 | 是否允许突发 | 典型场景 | 我踩过的坑 |
|---|---|---|---|---|
| 漏桶 | 强制匀速输出 | 否 | 日志写入、消息生产 | 突发流量全丢,业务方投诉 |
| 令牌桶 | 允许短时突发 | 是(受桶容量限制) | API 网关、接口限流 | 令牌生成速率没算对,导致限流过严 |
| 滑动窗口 | 精确控制时间窗口 | 否(窗口内总量限制) | 防刷、秒杀、优惠券 | 格子数设太多,内存暴涨 |
1.5 知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当作流控的「地图」:
这张图把本章的三个核心问题串在了一起。你从上往下看:先搞清楚「什么是流控」,再理解「为什么需要」,最后落到「怎么实现」。后面的章节,我们会基于这些算法,搭建真正的调试工具链。
我的建议:别急着背代码。先想清楚你的业务场景——是保护数据库?还是防刷?还是平滑流量?场景定了,算法自然就选出来了。