典型回答 题外话:这个问题,我在14年找实习的时候,被易保支付这家公司问过,当时面试通过了,但是我没去。没想到10年了,我最近看面经的时候,这家公司还在问这个问题。当年我回答的还行,但是现在让我回答肯定能回答的更好了。
这个问题,其实主要实现2个关键功能即可。
1、限制用户5分钟内最多尝试登录3次。
2、对用户进行锁定。
第一个功能,其实是一个典型的滑动窗口问题。
✅什么是滑动窗口限流?
✅如何基于Redis实现滑动窗口限流?
我们只需要构造一个滑动窗口,窗口大小限制5分钟,然后限流次数设置为3次即可实现这个功能了。而滑动窗口我们可以借助Redis来实现。
而第二个功能,我们只需要把被锁定的用户保存在Redis中即可,这样还能根据业务要求,设置一个合理的超时时间。
主要的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import redis.clients.jedis.Jedis; public class SlidingWindowRateLimiter { private Jedis jedis; private String key; private int limit = 3; //限制请求次数最大3次 private int lockTime; // 锁定用户的时间,单位:秒 public SlidingWindowRateLimiter(Jedis jedis, String key, int limit, int lockTime) { this.jedis = jedis; this.key = key; this.limit = limit; this.lockTime = lockTime; // 锁定时间 } public boolean allowRequest() { // 当前时间戳,单位:毫秒 long currentTime = System.currentTimeMillis(); // 锁定键的名称(锁定的用户) String lockKey = "lock:" + key; // 检查用户是否已被锁定 if (jedis.exists(lockKey)) { return false; // 用户已被锁定,返回 false } // 使用Lua脚本来确保原子性操作 String luaScript = "local window_start = ARGV[1] - 300000\n" + // 计算5分钟的起始时间 "redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)\n" + // 清理过期的请求 "local current_requests = redis.call('ZCARD', KEYS[1])\n" + // 获取当前请求次数 "if current_requests < tonumber(ARGV[2]) then\n" + // 如果请求次数小于限制 " redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])\n" + // 添加当前请求时间 " return 1\n" + // 允许请求 "else\n" + " redis.call('SET', 'lock:'..KEYS[1], 1, 'EX', tonumber(ARGV[3]))\n" + // 锁定用户 " return 0\n" + // 拒绝请求 "end"; // 调用 Lua 脚本进行原子操作 Object result = jedis.eval(luaScript, 1, key, String.valueOf(currentTime), String.valueOf(limit), String.valueOf(lockTime)); // 返回操作结果 return (Long) result == 1; } } 滑动窗口:滑动窗口的逻辑依然和原来一样,使用 Redis 有序集合(ZADD)记录每次登录尝试的时间戳,过期的记录会被自动清理。
...