需求:
这是昨天面试中遇到的一个问题,当用户 x 秒内请求 y 次,禁止用户访问
先写一些我自己想的方案,下面的windowStart
为 x 秒之前的 timestamp,current
为现在的 timestamp,threshold
为请求次数限制。下列方法都需要设置和更新 key 过期时间,以免造成 key 泄漏。
方案 1:
用 Redis 的 List 数据结构记录用户访问的时间
当用户访问时 rpush 当前时间戳并删掉左侧 x 秒之前的时间戳,此时该列表的长度为 x 秒请求次数,注意此方法的timestamp
需要用 Redis 的 Lua 脚本否则可能因为各机器时间不同出问题(例如某台机器时间为未来,则 ltrim 不到该条数据后面的正常时间戳)
伪代码:
redis.rpush(key, current);
redis.expire(key, current + y);
counter = 0;
while (redis.get(key, 0) < windowStart) {
counter++;
}
redis.ltrim(0, counter);
if (redis.llen(key) > y) {
doBanUser();
}
方案 1 的问题:
只需要最多存 y 条数据,但是这里用到了 List,Redis 中的 List 为双向链表,每个节点包含两个指针,在 64 位机器上占用 128bit,而实际有用的时间戳只要 64bit 。迭代链表时会更慢一些。
方案 2:
使用数组 ringbuffer 代替方案一的链表。
方案 2 的问题:
需要自己开发 Redis module,开发和运维成本较高。另外方案 1 和方案 2 中均存不方便重试和测试的问题。例如其他业务调自己服务但是自己服务超时,此时业务方重试,这种情况不应该算用户访问了两次。
方案 3:
使用 Redis 的 ZSet 数据结构
以用户请求的唯一 requestId 为 member,当前时间为 score,如果 requestId 已存在( logn 复杂度),不进行任何操作,如果不存在,则 ZADD,然后使用 ZRANGEBYSCORE 查看该时间段内请求数量。该方案同时需要使用 ZREMRANGEBYSCORE 来删除过老的元素,我认为可以每次或 N 次请求的时候删除,面试官说有更好的删除时机,但是我也没有问,这个问题就这么过去了。
想问问 V 友该方法应该如何删除,或者有没有更好的方法来用 Redis 限制用户行为频率?
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.