这里实现一分钟内只允许5次登录,废话不多说直接上代码:
1.使用 Lua 脚本 将“清理过期数据 → 统计计数 → 判断是否超限 → 添加新记录”四个操作封装为一个原子操作,避免高并发下 count 与 add 之间的竞争条件。
-- KEYS[1] : 限流key,例如 "login:limit:phone:138****0000"
-- ARGV[1] : 当前时间戳(毫秒)
-- ARGV[2] : 窗口大小(毫秒)
-- ARGV[3] : 限流阈值(最大请求次数)
-- ARGV[4] : 本次请求的唯一标识(例如时间戳+随机数)
local window_start = tonumber(ARGV[1]) - tonumber(ARGV[2])
-- 1. 删除窗口外的旧数据
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, window_start)
-- 2. 统计窗口内当前请求数
local current_count = redis.call('ZCARD', KEYS[1])
-- 3. 判断是否超过阈值
if current_count < tonumber(ARGV[3]) then
-- 4. 添加本次请求记录
redis.call('ZADD', KEYS[1], ARGV[1], ARGV[4])
-- 5. 设置 key 过期时间(窗口大小 + 1秒缓冲)
redis.call('EXPIRE', KEYS[1], math.ceil(tonumber(ARGV[2]) / 1000) + 1)
return 1 -- 放行
else
return 0 -- 限流
end
2.新建一个limit包,编写服务如下,也可以写在service实现类中:
package com.hmdp.limit;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
@Service
public class LoginLimitService {
@Resource
private StringRedisTemplate stringRedisTemplate;
//定义一个redis lua脚本,返回long类型数据
private DefaultRedisScript<Long> loginLimitScript;
private static final long WINDOW_SIZE_MS = 60 * 1000L; // 1分钟 窗口
private static final int LIMIT_COUNT = 5;
@PostConstruct
public void init() {
loginLimitScript = new DefaultRedisScript<>();
// 加载 resources/lua/login_limit.lua
loginLimitScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("login_limit.lua")));
loginLimitScript.setResultType(Long.class);
}
public boolean tryAcquire(String phoneNumber) {
String key = "login:limit:phone:" + phoneNumber;
long now = System.currentTimeMillis(); //获取当前系统时间的时间戳
String member = now + "_" + UUID.randomUUID().toString(); //在zset中member如果一样 后写的会覆盖前写的 保证member的唯一
Long result = stringRedisTemplate.execute(
loginLimitScript,
Collections.singletonList(key),
String.valueOf(now),
String.valueOf(WINDOW_SIZE_MS),
String.valueOf(LIMIT_COUNT),
member
);
return result != null && result == 1L;
}
}
注意这里StringRedisTemplate需要配置序列化器
3.登录接口:
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session, HttpServletRequest request){
String phone =loginForm.getPhone();
// 实现登录功能
if ( phone== null){
phone = IpUtils.getIp( request); //拿到ip 作为手机号
}
//添加滑动窗口进行登录限流 每一分钟 最多5次
if (!loginLimitService.tryAcquire(phone)) {
return Result.fail("登录尝试过于频繁,请稍后再试");
}
return userService.login(loginForm, session);
}
4.如果请求手机号是空的,拿到请求ip做key,在config包下添加iputils如下:
package com.hmdp.utils;
import javax.servlet.http.HttpServletRequest;
//获取ip的工具类
public class IpUtils {
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 取第一个IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
当然如果手机号码是强校验也可以省略ip来做key
5.postman做测试:在连续点击5次登录请求之后,可以看到直接返回了登录过于频繁

注意401 要带请求token
6.redis中窗口如图所示:


1940

被折叠的 条评论
为什么被折叠?



