一、生产环境中令牌桶算法适配突发流量的核心逻辑
很多开发者都遇到过这种场景:周末电商大促,商品详情页瞬间涌入上万请求,刚上线的限流规则要么把正常请求误杀,要么直接压垮后端数据库。令牌桶算法因为“平稳限流+突发适配”的特性,成了这种场景的首选,但直接用单机版本会踩坑,得结合生产环境做适配优化。
1.1 从日常小例子看懂令牌桶的本质
咱们把令牌桶比作公司的请假审批规则:正常情况下,HR每秒审批2个请假申请(对应令牌桶的生成速率),最多存5个审批名额(对应桶的最大容量,应对突发的请假请求)。平时没人请假,名额会慢慢攒到5个,要是某天突然有4个同事同时请假(突发流量),可以直接用攒的名额快速过,后续再慢慢补回名额。但如果是分布式的公司,每个部门有自己的HR(单机令牌桶),总审批名额就会失控,得把所有名额统一放到一个全局HR池里(分布式令牌桶)。
二、生产环境令牌桶适配的核心逻辑
生产环境的核心不是“有令牌桶”,而是“分布式下的令牌桶能控住总流量”,同时兼顾突发流量的缓冲。
2.1 单机令牌桶的基础实现与局限
先看最简单的单机令牌桶代码,用Python实现,注释全是生活化说明:
# 技术栈:Python 3.10
import time
class SimpleTokenBucket:
def __init__(self, rate=2, capacity=5):
self.rate = rate # 每秒生成的令牌数(HR每秒批2个假)
self.capacity = capacity # 桶最大容量(最多攒5个假名额)
self.tokens = capacity # 初始令牌全满
self.last_update = time.time() # 上次补令牌的时间
def get_token(self):
# 先补令牌:计算距离上次过了多久,生成对应令牌,最多不超桶容量
now = time.time()
elapsed = now - self.last_update
self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
self.last_update = now
# 有令牌就拿,返回True;没有就限流,返回False
if self.tokens >= 1:
self.tokens -= 1
return True
return False
# 测试代码
if __name__ == "__main__":
tb = SimpleTokenBucket(rate=2, capacity=5)
# 模拟前5个突发请求,都能过(令牌满)
for i in range(5):
print(f"第{i+1}个请求:{'通过' if tb.get_token() else '被限流'}")
# 第6个请求,令牌用完,限流
print(f"第6个请求:{'通过' if tb.get_token() else '被限流'}")
# 等1秒,补2个令牌,后面2个请求通过
time.sleep(1)
for i in range(2):
print(f"第{i+7}个请求:{'通过' if tb.get_token() else '被限流'}")
这个单机版本适合单实例的小服务,但放到分布式微服务里就会失效:比如3个部署实例,每个实例单独跑这个代码,总限流本该是每秒6个,实际可能每秒有10个请求通过,总流量失控压垮下游。
2.2 分布式环境的核心适配:全局令牌桶
要解决分布式的问题,得把令牌放到所有实例共享的存储里,比如Redis,相当于把审批名额放到集团HR池,所有部门一起用。这里的关键是原子操作,避免多个实例同时抢令牌出错,用Lua脚本在Redis里实现:
# 技术栈:Python 3.10 + Redis 6.0(依赖Redis作为全局存储)
import time
import redis
import json # 用于序列化令牌数据
class GlobalTokenBucket:
def __init__(self, redis_host="localhost", redis_port=6379, rate=6, capacity=15):
self.redis = redis.Redis(host=redis_host, port=redis_port, db=0, decode_responses=True)
self.rate = rate # 全局每秒生成的令牌数(总限流)
self.capacity = capacity # 全局突发最大令牌数(应对大促峰值)
self.bucket_key = "global_service_token" # Redis里存令牌的键
def get_token(self):
# Lua脚本:原子执行补令牌+扣令牌,避免并发冲突
lua_script = """
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local key = KEYS[1]
local data = redis.call('GET', key)
local tokens, last_update = 0, now
-- 解析已有令牌数据
if data then
tokens, last_update = unpack(json.decode(data))
-- 计算该补多少令牌,最多不超桶容量
local elapsed = now - last_update
tokens = math.min(capacity, tokens + elapsed * rate)
else
tokens = capacity -- 首次初始化,令牌全满
end
-- 扣令牌,更新到Redis,原子操作保证不会抢出错
if tokens >= 1 then
tokens = tokens - 1
redis.call('SET', key, json.encode({tokens, now}))
return 1
else
return 0
end
"""
# 执行Lua脚本,参数对应Redis键和业务参数
result = self.redis.eval(lua_script, 1, self.bucket_key, self.rate, self.capacity, now)
return result == 1
# 测试:模拟分布式实例拿令牌,总流量可控
if __name__ == "__main__":
gtb = GlobalTokenBucket()
# 前10个请求,总令牌最大15,所以前10个都能过
for i in range(10):
print(f"请求{i+1}:{'通过' if gtb.get_token() else '被限流'}")
# 等2秒,令牌补了12个,剩余令牌会被更新,后续请求正常走
time.sleep(2)
for i in range(5):
print(f"请求{i+11}:{'通过' if gtb.get_token() else '被限流'}")
这个适配的核心是:把令牌的生成、扣减都放到Redis里用原子操作执行,所有分布式实例都读同一个全局令牌池,既保证了总流量不超,又能应对突发的峰值。
三、架构优化思路
光有全局令牌桶还不够,生产环境还要做细节优化,才能扛住大流量。
3.1 令牌桶的预热优化
大促前10-15分钟,把Redis里的全局令牌桶手动补满,相当于提前攒够峰值的审批名额,大促峰值来了就不用等慢慢补令牌,直接让所有请求通过,避免刚到峰值就被限流。比如618前,把capacity设为平时峰值的1.5倍,提前10分钟给Redis发指令补满令牌,效果立竿见影。
3.2 降级与容错处理
要是Redis挂了,全局令牌桶就会失效,得做降级方案:比如Redis异常时,临时用单机令牌桶,或者把请求全放行(等Redis恢复后再补算之前的令牌),避免因为限流组件挂了导致服务不可用。比如电商大促时,核心接口的降级策略应该是“宁漏杀、不宕机”,保证服务能活下来。
四、应用场景、优缺点与注意事项
4.1 应用场景
这个适配后的全局令牌桶适合这些场景:1. 电商大促的商品详情页、下单接口,应对突发的用户请求;2. 短信、推送等消息服务,避免短时间发太多被运营商限制;3. API网关的上游流量控制,比如第三方调用本系统的接口,限制总调用量;4. 数据库的流量入口,避免查询量突增压垮数据库。
4.2 技术优缺点
优点:1. 完美兼顾突发流量,capacity能临时扛峰值,rate能保证日常平稳;2. 分布式下总流量可控,不会出现单点失控;3. 逻辑简单,容易调试和修改。缺点:1. 依赖Redis,Redis的性能和可用性直接影响限流;2. 要是参数(rate、capacity)设得不合理,要么浪费资源,要么扛不住峰值;3. Lua脚本的逻辑太复杂的话,排查问题会麻烦。
4.3 注意事项
- Redis必须用高可用集群:不能用单节点Redis,要用哨兵或集群模式,避免Redis单点故障导致限流失效;2. 参数要根据业务调:rate设为日常正常请求的峰值,capacity设为突发请求的峰值,比如日常每秒请求2000,大促峰值10000,就设rate=2000,capacity=10000;3. 加监控:监控Redis里令牌桶的剩余数量、请求通过率,要是剩余令牌少于10%,说明快到限流,要提前调整参数;4. 避免令牌桶冻结:要是服务长时间没流量,令牌桶的last_update时间会很久,导致计算elapsed时报错,代码里要加逻辑处理这种情况。
五、总结
生产环境中用令牌桶适配突发流量,核心是从“单机令牌桶”升级为“分布式全局令牌桶”,用Redis的原子操作避免并发冲突,再结合预热、降级等优化手段,就能既应对突发流量,又保证总流量不压垮后端服务。这个方案门槛不高,代码实现也简单,只要注意Redis的高可用和参数调整,就能适配大部分生产场景。
评论
围绕“生产环境中令牌桶算法突发流量适配的核心逻辑与架构优化思路”参与讨论