这里实现一分钟内只允许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 -- 限流 end2.新建一个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