介绍
在互联网应用中,流量控制是非常关键的一环。流量过大会导致系统崩溃,流量过小会导致用户体验不佳。因此,限流是保障系统稳定性和用户体验的一种重要手段。
在分布式系统中,限流需要考虑多个节点的流量控制,这就需要用到分布式限流方案。本文将介绍基于 Redis 的分布式限流方案实现。
Redis 的限流方案
Redis 是一个高性能的内存数据库,支持多种数据结构,其中包括计数器和有序集合。这两种数据结构可以用来实现限流。
计数器限流
计数器限流是最简单的限流方案之一。它通过对某个时间窗口内的请求计数,来判断是否超过了限制。如果超过了限制,就拒绝请求。
计数器限流的实现非常简单,可以用 Redis 的 INCR 命令实现。每来一个请求,就将计数器加 1,然后与限制值进行比较。如果超过了限制值,就拒绝请求。
// javascriptcn.com 代码示例 import redis r = redis.Redis(host='localhost', port=6379, db=0) def is_allowed(key, limit, period): # 获取当前时间戳 now = int(time.time()) # 计算时间窗口的起始时间戳 start = now - period # 获取时间窗口内的请求数量 count = r.get(key) if count is None: # 如果计数器不存在,则创建一个新的计数器,并设置过期时间 r.setex(key, period, 1) return True elif int(count) < limit: # 如果请求数量没有超过限制,则将计数器加 1 r.incr(key) return True else: # 如果请求数量超过限制,则拒绝请求 return False
上面的代码中,我们将计数器的键名作为参数传入。如果计数器不存在,则创建一个新的计数器,并设置过期时间为时间窗口的长度。如果计数器存在,则通过 INCR 命令将计数器加 1。如果计数器的值超过了限制,则拒绝请求。
有序集合限流
有序集合限流是一种更加高级的限流方案。它通过有序集合来保存请求的时间戳和计数器值,然后使用 Redis 的 ZREMRANGEBYSCORE 命令来删除过期的请求。这种方案可以更加精确地控制请求的数量。
有序集合限流的实现需要用到 Redis 的 ZADD、ZCARD 和 ZREMRANGEBYSCORE 命令。首先,每来一个请求,就将请求的时间戳作为有序集合的分值,将计数器值作为有序集合的成员。然后,使用 ZCARD 命令获取有序集合的长度,如果长度超过了限制,则使用 ZREMRANGEBYSCORE 命令删除过期的请求。
// javascriptcn.com 代码示例 import redis r = redis.Redis(host='localhost', port=6379, db=0) def is_allowed(key, limit, period): # 获取当前时间戳 now = int(time.time()) # 计算时间窗口的起始时间戳 start = now - period # 将请求的时间戳作为有序集合的分值,将计数器值作为有序集合的成员 r.zadd(key, {now: now}) # 使用 ZREMRANGEBYSCORE 命令删除过期的请求 r.zremrangebyscore(key, 0, start) # 使用 ZCARD 命令获取有序集合的长度 count = r.zcard(key) if count <= limit: return True else: return False
上面的代码中,我们将有序集合的键名作为参数传入。每来一个请求,就将请求的时间戳作为有序集合的分值,将计数器值作为有序集合的成员。然后,使用 ZREMRANGEBYSCORE 命令删除过期的请求。最后,使用 ZCARD 命令获取有序集合的长度,如果长度超过了限制,则拒绝请求。
分布式限流方案
在分布式系统中,需要考虑多个节点的流量控制。这就需要用到分布式限流方案。基于 Redis 的分布式限流方案可以通过 Redis 的 Lua 脚本来实现。
计数器限流
计数器限流的分布式实现可以将计数器的键名加上节点标识符,然后通过 Redis 的 EVALSHA 命令执行 Lua 脚本来实现。
// javascriptcn.com 代码示例 local key = KEYS[1] local limit = tonumber(ARGV[1]) local period = tonumber(ARGV[2]) local now = tonumber(redis.call('time')[1]) local start = now - period local count = tonumber(redis.call('get', key)) if not count then redis.call('setex', key, period, 1) return 1 elseif count < limit then redis.call('incr', key) return 1 else return 0 end
上面的 Lua 脚本中,我们将计数器的键名作为参数传入,并加上节点标识符。然后,通过 Redis 的 GET 命令获取计数器的值,如果计数器不存在,则创建一个新的计数器,并设置过期时间为时间窗口的长度。如果计数器存在,则通过 INCR 命令将计数器加 1。如果计数器的值超过了限制,则返回 0,否则返回 1。
// javascriptcn.com 代码示例 import redis r = redis.Redis(host='localhost', port=6379, db=0) def is_allowed(key, limit, period): # 获取当前时间戳 now = int(time.time()) # 计算时间窗口的起始时间戳 start = now - period # 将节点标识符加到计数器的键名中 key = key + ':' + str(node_id) # 通过 EVALSHA 命令执行 Lua 脚本 script = """ local key = KEYS[1] local limit = tonumber(ARGV[1]) local period = tonumber(ARGV[2]) local now = tonumber(redis.call('time')[1]) local start = now - period local count = tonumber(redis.call('get', key)) if not count then redis.call('setex', key, period, 1) return 1 elseif count < limit then redis.call('incr', key) return 1 else return 0 end """ sha1 = r.script_load(script) return bool(r.evalsha(sha1, 1, key, limit, period))
上面的代码中,我们将节点的标识符作为参数传入,并将节点的标识符加到计数器的键名中。然后,通过 EVALSHA 命令执行 Lua 脚本。如果 Lua 脚本返回 1,则允许请求,否则拒绝请求。
有序集合限流
有序集合限流的分布式实现需要将有序集合的键名加上节点标识符,并使用 Redis 的 EVALSHA 命令执行 Lua 脚本。
// javascriptcn.com 代码示例 local key = KEYS[1] local limit = tonumber(ARGV[1]) local period = tonumber(ARGV[2]) local now = tonumber(redis.call('time')[1]) local start = now - period redis.call('zadd', key, now, now) redis.call('zremrangebyscore', key, 0, start) local count = tonumber(redis.call('zcard', key)) if count <= limit then return 1 else return 0 end
上面的 Lua 脚本中,我们将有序集合的键名作为参数传入,并加上节点标识符。然后,将请求的时间戳作为有序集合的分值,将计数器值作为有序集合的成员。使用 ZREMRANGEBYSCORE 命令删除过期的请求。最后,使用 ZCARD 命令获取有序集合的长度,如果长度超过了限制,则返回 0,否则返回 1。
// javascriptcn.com 代码示例 import redis r = redis.Redis(host='localhost', port=6379, db=0) def is_allowed(key, limit, period): # 获取当前时间戳 now = int(time.time()) # 计算时间窗口的起始时间戳 start = now - period # 将节点标识符加到有序集合的键名中 key = key + ':' + str(node_id) # 通过 EVALSHA 命令执行 Lua 脚本 script = """ local key = KEYS[1] local limit = tonumber(ARGV[1]) local period = tonumber(ARGV[2]) local now = tonumber(redis.call('time')[1]) local start = now - period redis.call('zadd', key, now, now) redis.call('zremrangebyscore', key, 0, start) local count = tonumber(redis.call('zcard', key)) if count <= limit then return 1 else return 0 end """ sha1 = r.script_load(script) return bool(r.evalsha(sha1, 1, key, limit, period))
上面的代码中,我们将节点的标识符作为参数传入,并将节点的标识符加到有序集合的键名中。然后,通过 EVALSHA 命令执行 Lua 脚本。如果 Lua 脚本返回 1,则允许请求,否则拒绝请求。
总结
本文介绍了基于 Redis 的分布式限流方案实现。通过计数器和有序集合两种数据结构,我们可以实现简单和精确的限流。通过 Lua 脚本,我们可以实现分布式限流,保证多个节点的流量控制。这种方案可以在高并发的场景下保障系统的稳定性和用户体验。
来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/657ec2c2d2f5e1655d99e87f