限流问题是分布式系统中无论如何都绕不开的一个话题,缓存穿透、缓存击穿、缓存雪崩这几个问题也是避不开的,这一篇就学习一下如何使用redis实现一个简单的限流。
限流不仅仅是控制流量,还有一点就是控制用户行为,一些很明显的非法请求(比如高并发的爬虫),就得制定相应的策略来处理。
限流的思路容易想到的一点就是限制请求频率,在一定时间内的请求次数过多就进行限制。这点容易想到的原因就是爬虫的时候会被限制IP,一部分原因也就是请求太过频繁就会被检测到异常(想起来当初用自己的账号爬某宝,结果后来用的时候点一下跳出一次验证码。。)。但是到底使用啥结构来实现呢?作者提出的思路是使用zset。
这种场景需要一个时间窗口来查看用户在这段时间内的请求次数,如果我们将时间作为zset的score,那么就可以直接查询指定score区间内的值的数量是否超过限制。
python版代码如下:
import time import redis client = redis.StrictRedis() def is_action_allowed(user_id, action_key, period, max_count): key = f'hist:{user_id}:{action_key}' # 使用毫秒时间戳 now_ts = int(time.time()*1000) with client.pipeline() as pipe: # 记录行为, score和value都使用时间戳 pipe.zadd(key, now_ts, now_ts) # 移除窗口时间之外的请求 pipe.zremrangebyscore(key, 0, now_ts-period*1000) pipe.zcard(key) # 设置zset的过期时间,避免冷用户持续占用内存 # 过期时间应该等于时间窗口长度,多宽限出1S pipe.expire(key, period + 1) # 批量执行 _, _, current_count, _ = pipe.execute() # 比较数量是否超标 return current_count <= max_count for i in range(20): print(is_action_allowed('user_id', 'action', 60, 5))
这段代码的整体思路其实并不复杂,每次进行判断的时候都维护一次对应user的时间窗口,窗口外的记录都清除,我们判断的依据是score,因此score的值是很重要的,value的值就没有什么特别的意义。
文中指出该中种方式的连续几个操作都是针对同一个key的,使用pipeline可以大大提高效率。但是这种方式的缺点也是很明显的,如果单位时间内的允许的请求次数非常高的话,将会浪费大量的内存,因此这种方法的局限性很大。后文中将继续介绍其他几种限流方案。