✅5 分钟内最多允许用户尝试登录 3 次,如果错误次数超过限制,需要对该用户进行锁定。如何实现?

✅5 分钟内最多允许用户尝试登录 3 次,如果错误次数超过限制,需要对该用户进行锁定。如何实现?

典型回答

题外话:这个问题,我在14年找实习的时候,被易保支付这家公司问过,当时面试通过了,但是我没去。没想到10年了,我最近看面经的时候,这家公司还在问这个问题。当年我回答的还行,但是现在让我回答肯定能回答的更好了。

这个问题,其实主要实现2个关键功能即可。

1、限制用户5分钟内最多尝试登录3次。

2、对用户进行锁定。

第一个功能,其实是一个典型的滑动窗口问题。

✅什么是滑动窗口限流?

✅如何基于Redis实现滑动窗口限流?

我们只需要构造一个滑动窗口,窗口大小限制5分钟,然后限流次数设置为3次即可实现这个功能了。而滑动窗口我们可以借助Redis来实现。

而第二个功能,我们只需要把被锁定的用户保存在Redis中即可,这样还能根据业务要求,设置一个合理的超时时间。

主要的实现代码如下:

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)记录每次登录尝试的时间戳,过期的记录会被自动清理。

  • ZREMRANGEBYSCORE 命令会移除当前时间窗口外的记录,确保统计的是最近 5 分钟内的登录尝试次数。

允许请求:如果在 5 分钟内的请求次数没有超过限制,脚本会将当前请求的时间戳添加到 Redis 的有序集合中,并返回 1,表示允许请求。

拒绝请求:如果用户在 5 分钟内的请求次数超过限制,脚本会设置用户的锁定键,并返回 0,表示拒绝请求。


用户锁定:在 Lua 脚本中,我们使用 SET 命令在 Redis 中设置一个名为 lock:<user_id> 的键,表示该用户已经被锁定。当尝试次数超过限制时,设置这个锁,并给它一个过期时间(lockTime)。

  • 锁定的用户无法再继续尝试登录,allowRequest() 方法返回 false,表示该用户被锁定。