接口每秒限制客户端访问200次
基于应用层限流,目的是:防止用户恶意刷新接口,保证程序稳定,高可用
Lua优点:
减少网络开销:这个脚本只要执行一次,能减少网络传输
原子性:Redis将这个脚本作为原子执行要么全部成功或者失败,不担心并发问题,不需要事务,(PS:LUA脚本保证原子性,执行lua脚本时不会同时执行其它脚本或redis命令, 这种语义类似于MULTI/EXEC,这个lua脚本要么执行成功,要么执行失败)
复用性:lua一旦执行就能永久性保存Redis的数据,可以供其它客户端使用
创建lua,放到resource下myLimit_ip.lua
-- 为某个接口的请求IP设置计数器,比如:127.0.0.1请求查询用户接口
-- KEYS[1] = 127.0.0.1 也就是用户的IP
-- ARGV[1] = 过期时间 1s
-- ARGV[2] = 限制的次数
local count = redis.call('incr',KEYS[1]);
if count == 1 then
redis.call("expire",KEYS[1],ARGV[2])
end
-- 如果时间还没有过期,并且还在规定的次数内,则请求同一接口
if count > tonumber(ARGV[1]) then
return false
end
return true
MyLuaConfiguration读取lua
@Configuration
public class MyLuaConfiguration {
/**
* 将lua脚本的内容加载出来放入到DefaultRedisScript
* @author fan
* @date 2022/5/7 2:35
* @return org.springframework.data.redis.core.script.DefaultRedisScript<java.lang.Boolean>
*/
@Bean
public DefaultRedisScript<Boolean> ipLimitLua() {
DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("myLimit_ip.lua")));
defaultRedisScript.setResultType(Boolean.class);
return defaultRedisScript;
}
}
MyAcessLimter
/**创建自定义的限流注解在Controller使用
* @author fan
* @date 2022年05月07日 1:08
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAcessLimter {
/**
* 每timeout限制请求的个数
* @author fan
* @date 2022/5/7 1:54
* @return int
*/
int count() default 5;
/**
* 超时时间,单位默认是秒
* @author fan
* @date 2022/5/7 1:54
* @return int
*/
int timeout() default 10;
/**
* 访问间隔
* @author fan
* @date 2022/5/7 1:54
* @return int
*/
int waits () default 20;
}
MyAcessLimiterAspect
/**
* @author fan
* @date 2022年05月07日 1:07
*/
@Component
@Aspect
@Slf4j
public class MyAcessLimiterAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private DefaultRedisScript<Boolean> myClientIPLimitLua;
// 1: 切入点 创建的注解类
@Pointcut("@annotation(com.fan.li.limit.MyAcessLimter)")
public void myLimiterPonicut() {
}
@Before("myLimiterPonicut()")
public void limiter(JoinPoint joinPoint) {
log.info("限流开始......." + LocalDate.now());
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String classname = methodSignature.getMethod().getDeclaringClass().getName();
String packageName = methodSignature.getMethod().getDeclaringClass().getPackage().getName();
log.info("method:{},classname:{},packageName:{}",method,classname,packageName);
MyAcessLimter annotation = method.getAnnotation(MyAcessLimter.class);
String methodNameKey = method.getName();
log.info("获取注解方法名:{}",methodNameKey);
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
HttpServletResponse response = requestAttributes.getResponse();
String userClientIp = MyClientIPUtils.getIpAddr(request);
log.info("当前用户IP:{}", userClientIp );
Integer count = annotation.count();
Integer timeout = annotation.timeout();
/*Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
log.info("参数id--> " + request.getParameter("userId") + "---" + args[i]);
}*/
String key = request.getParameter("userId") + ":" + userClientIp ;//这里用userId + userClientIp 作为key
log.info("当前的key-->" + key);
Boolean b = stringRedisTemplate.execute(myClientIPLimitLua, Lists.newArrayList(key), count.toString(), timeout.toString());//读取lua
if (!b) {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
try (PrintWriter writer = response.getWriter();) {
writer.print("<h1>操作频繁,请稍后在试</h1>");
} catch (Exception ex) {
throw new RuntimeException("操作频繁,请稍后在试");
}
}
}
}
MyClientIPUtils
/**获取真实访问的IP
* @author fan
* @date 2022年05月07日 1:06
*/
public class MyClientIPUtils{
public static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
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("X-Forwarded-For");
}
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.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
}
MyLimiterController
/**
* 描述: 测试限流
*
* @author fan
* @create 2022年05月07日 1:59
**/
@RestController
@RequestMapping(value = "limit")
public class MyLimiterController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UserService userService;
/**
* 接口每秒限制访问200次
* @author fan
* @date 2022/5/7 2:00
* @return list
*/
@GetMapping("/index")
@MyAcessLimter(count = 200,timeout = 1)
public List<User> index(String userId) {//对需要做限流的接口
List<User> list = userService.selectUserById(Integer.parseInt(userId));
//list.get(0);
return list;
}
}
效果图:把次数200改成5测试
1S内操作次数超过5次就会提示: