共计 2695 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
在分布式 AI 服务中,会话上下文管理是一个常见但具有挑战性的问题。传统的单机内存存储方案在面对高并发场景时,往往会遇到以下几个问题:

- 节点间状态不一致:在多节点部署时,用户的会话请求可能被路由到不同的服务器,导致上下文信息丢失或不一致。
- 内存溢出风险:随着会话数量的增加,单机内存可能无法承载所有会话数据,导致服务崩溃。
- 缺乏动态调整能力:传统的固定窗口限流无法适应突发流量的变化,容易造成资源浪费或服务过载。
技术选型
为了解决这些问题,我们对比了几种常见的分布式缓存方案:
- Memcached:虽然性能不错,但缺乏丰富的数据结构和原子操作支持,不适合实现复杂的流控逻辑。
- 本地缓存:无法解决节点间状态同步问题,且内存管理复杂。
- Redis:提供了丰富的数据结构(如 Sorted Set)、原子操作(Lua 脚本)和灵活的 TTL 管理,是理想的解决方案。
核心实现
使用 Redis Sorted Set 实现动态滑动窗口
滑动窗口算法是流控的核心,我们利用 Redis 的 Sorted Set 来实现:
import time
import redis
class SlidingWindow:
def __init__(self, redis_conn, window_size=60, max_requests=100):
self.redis = redis_conn
self.window_size = window_size # 窗口大小(秒)self.max_requests = max_requests # 最大请求数
def allow_request(self, user_id):
current_time = time.time()
key = f"rate_limit:{user_id}"
# 移除过期请求
self.redis.zremrangebyscore(key, 0, current_time - self.window_size)
# 获取当前窗口内的请求数
count = self.redis.zcard(key)
if count < self.max_requests:
# 添加当前请求
self.redis.zadd(key, {current_time: current_time})
self.redis.expire(key, self.window_size)
return True
return False
令牌桶算法的分布式改造
为了应对突发流量,我们在令牌桶算法基础上加入分布式支持:
import redis
class TokenBucket:
def __init__(self, redis_conn, capacity=100, refill_rate=1):
self.redis = redis_conn
self.capacity = capacity
self.refill_rate = refill_rate # 令牌 / 秒
def get_token(self, user_id):
lua_script = """
local key = KEYS[1]
local now = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local refill_rate = tonumber(ARGV[3])
local last_time = tonumber(redis.call('get', key..':time')) or now
local tokens = tonumber(redis.call('get', key..':tokens')) or capacity
local delta = math.max(0, now - last_time)
tokens = math.min(capacity, tokens + delta * refill_rate)
if tokens >= 1 then
tokens = tokens - 1
redis.call('set', key..':time', now)
redis.call('set', key..':tokens', tokens)
return 1
end
return 0
"""key = f"token_bucket:{user_id}"
result = self.redis.eval(lua_script, 1, key, time.time(), self.capacity, self.refill_rate)
return bool(result)
性能优化
测试不同窗口大小对内存占用的影响
我们通过压测发现,窗口大小与内存占用呈线性关系。较小的窗口(如 30 秒)能显著降低内存使用,但可能增加误判率。
管道化 (pipeline) 操作
使用 Redis 的 pipeline 可以减少网络往返,提升性能:
def batch_allow_requests(self, user_ids):
pipe = self.redis.pipeline()
current_time = time.time()
for user_id in user_ids:
key = f"rate_limit:{user_id}"
pipe.zremrangebyscore(key, 0, current_time - self.window_size)
pipe.zcard(key)
results = pipe.execute()
allowed = []
for i in range(0, len(results), 2):
user_id = user_ids[i//2]
count = results[i+1]
if count < self.max_requests:
allowed.append(user_id)
return allowed
避坑指南
- Redis 集群模式下 slot 分配问题:确保相关 key 分布在同一个 slot 上,可以通过 hash tag 实现,如
{user_id}:rate_limit。 - 时钟漂移对 TTL 的影响:不同服务器时钟不一致可能导致 TTL 计算错误,建议使用 Redis 的 TIME 命令获取统一时间。
- 热点 key 的解决方案:对于高频访问的 key,可以考虑本地缓存 + 定期同步的策略减少 Redis 压力。
延伸思考
当前的滑动窗口算法适合处理均匀流量,但对于突发流量的处理可能不够理想。读者可以尝试结合 Leaky Bucket 算法来改进:
- Leaky Bucket 以恒定速率处理请求,能更好地平滑突发流量。
- 实现时可以用 Redis 的 LIST 结构模拟队列,配合 Lua 脚本保证原子性。
总结
通过 Redis 实现的分布式会话流控方案,我们解决了传统内存存储的局限性,同时保持了高性能和可扩展性。动态滑动窗口和令牌桶算法的结合,使得系统能够灵活应对各种流量场景。希望本文能为开发者提供实用的参考,欢迎大家尝试并优化这一方案。
正文完
发表至: 未分类
近一天内
