分布式---基于Redis进行接口IP限流

场景 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即一定时间内同一IP访问的次数是有限的。

实现原理 用Redis作为限流组件的核心的原理,将用户的IP地址当Key,一段时间内访问次数为value,同时设置该Key过期时间。

比如某接口设置相同IP10秒内请求5次,超过5次不让访问该接口。

1. 第一次该IP地址存入redis的时候,key值为IP地址,value值为1,设置key值过期时间为10秒。
2. 第二次该IP地址存入redis时,如果key没有过期,那么更新value为2。
3. 以此类推当value已经为5时,如果下次该IP地址在存入redis同时key还没有过期,那么该Ip就不能访问了。
4. 当10秒后,该key值过期,那么该IP地址再进来,value又从1开始,过期时间还是10秒,这样反反复复。

说明从上面的逻辑可以看出,是一时间段内访问次数受限,不是完全不让该IP访问接口。

技术框架 SpringBoot + RedisTemplate (采用自定义注解完成)

这个可以用于真实项目开发场景。

一、代码

1、自定义注解

这边采用自定义注解的目的就是,在接口上使用自定义注解,让代码看去非常整洁。

IpLimiter

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IpLimiter {
/**
* 限流ip
*/
String ipAdress() ;
/**
* 单位时间限制通过请求数
*/
long limit() default 10;
/**
* 单位时间,单位秒
*/
long time() default 1;
/**
* 达到限流提示语
*/
String message();
}

2、测试接口

在接口上使用了自定义注解@IpLimiter

@Controller
public class IpController { private static final Logger LOGGER = LoggerFactory.getLogger(IpController.class);
private static final String MESSAGE = "请求失败,你的IP访问太频繁"; //这里就不获取请求的ip,而是写死一个IP
@ResponseBody
@RequestMapping("iplimiter")
@IpLimiter(ipAdress = "127.198.66.01", limit = 5, time = 10, message = MESSAGE)
public String sendPayment(HttpServletRequest request) throws Exception {
return "请求成功";
}
@ResponseBody
@RequestMapping("iplimiter1")
@IpLimiter(ipAdress = "127.188.145.54", limit = 4, time = 10, message = MESSAGE)
public String sendPayment1(HttpServletRequest request) throws Exception {
return "请求成功";
}
}

3、处理IpLimter注解的AOP

这边采用切面的方式处理自定义注解。同时为了保证原子性,这边写了redis脚本ipLimiter.lua来执行redis命令,来保证操作原子性。

@Aspect
@Component
public class IpLimterHandler { private static final Logger LOGGER = LoggerFactory.getLogger(IpLimterHandler.class); @Autowired
RedisTemplate redisTemplate; /**
* getRedisScript 读取脚本工具类
* 这里设置为Long,是因为ipLimiter.lua 脚本返回的是数字类型
*/
private DefaultRedisScript<Long> getRedisScript; @PostConstruct
public void init() {
getRedisScript = new DefaultRedisScript<>();
getRedisScript.setResultType(Long.class);
getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ipLimiter.lua")));
LOGGER.info("IpLimterHandler[分布式限流处理器]脚本加载完成");
} /**
* 这个切点可以不要,因为下面的本身就是个注解
*/
// @Pointcut("@annotation(com.jincou.iplimiter.annotation.IpLimiter)")
// public void rateLimiter() {} /**
* 如果保留上面这个切点,那么这里可以写成
* @Around("rateLimiter()&&@annotation(ipLimiter)")
*/
@Around("@annotation(ipLimiter)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("IpLimterHandler[分布式限流处理器]开始执行限流操作");
}
Signature signature = proceedingJoinPoint.getSignature();
if (!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("the Annotation @IpLimter must used on method!");
}
/**
* 获取注解参数
*/
// 限流模块IP
String limitIp = ipLimiter.ipAdress();
Preconditions.checkNotNull(limitIp);
// 限流阈值
long limitTimes = ipLimiter.limit();
// 限流超时时间
long expireTime = ipLimiter.time();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("IpLimterHandler[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}", limitTimes, expireTime);
}
// 限流提示语
String message = ipLimiter.message();
/**
* 执行Lua脚本
*/
List<String> ipList = new ArrayList();
// 设置key值为注解中的值
ipList.add(limitIp);
/**
* 调用脚本并执行
*/
Long result = (Long) redisTemplate.execute(getRedisScript, ipList, expireTime, limitTimes);
if (result == 0) {
String msg = "由于超过单位时间=" + expireTime + "-允许的请求次数=" + limitTimes + "[触发限流]";
LOGGER.debug(msg);
// 达到限流返回给前端信息
return message;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("IpLimterHandler[分布式限流处理器]限流执行结果-result={},请求[正常]响应", result);
}
return proceedingJoinPoint.proceed();
}
}

4、RedisCacheConfig(配置类)

@Configuration
public class RedisCacheConfig { private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class); @Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper); template.setValueSerializer(serializer);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
LOGGER.info("Springboot RedisTemplate 加载完成");
return template;
}
}

5、ipLimiter.lua 脚本

优点

减少网络的开销: 脚本只执行一次,不需要发送多次请求, 减少网络传输;

保证原子操作: 整个脚本作为一个原子执行, 就不用担心并发问题;

--获取KEY
local key1 = KEYS[1] local val = redis.call('incr', key1)
local ttl = redis.call('ttl', key1) --获取ARGV内的参数并打印
local expire = ARGV[1]
local times = ARGV[2] redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire)) redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
if val == 1 then
redis.call('expire', key1, tonumber(expire))
else
if ttl == -1 then
redis.call('expire', key1, tonumber(expire))
end
end if val > tonumber(times) then
return 0
end
return 1

6、application.properties

#redis
spring.redis.hostName=
spring.redis.host=
spring.redis.port=6379
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=10
spring.redis.timeout=100ms
spring.redis.password= logging.path= /Users/xub/log
logging.level.com.jincou.iplimiter=DEBUG
server.port=8888

7、SpringBoot启动类

@SpringBootApplication
public class Application { public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

8、测试

完美上面这个测试非常符合我们的预期,前五次访问接口是成功的,后面就失败了,直到10秒后才可以重新访问,这样反反复复。

其它的这边就不一一展示了,附上该项目源码。

Github地址 https://github.com/yudiandemingzi/spring-boot-redis-ip-limiter

参考

这个设计是我在刷github的时候看到确实很好,我这边只是在它的基础上做了一些改动,非常感谢该作者的分享。

github地址:https://github.com/TaXueWWL/shleld-ratelimter

有关AOP有篇文章讲的不错:spring aop 中@annotation()的使用

只要自己变优秀了,其他的事情才会跟着好起来(中将1)

【分布式架构】--- 基于Redis组件的特性,实现一个分布式限流的更多相关文章

  1. 基于redis实现的四种常见的限流策略

    引言 在web开发中功能是基石,除了功能以外运维和防护就是重头菜了.因为在网站运行期间可能会因为突然的访问量导致业务异常.也有可能遭受别人恶意攻击 所以我们的接口需要对流量进行限制.俗称的QPS也是对 ...

  2. nginx+iis+redis+Task.MainForm构建分布式架构 之 (redis存储分布式共享的session及共享session运作流程)

    本次要分享的是利用windows+nginx+iis+redis+Task.MainForm组建分布式架构,上一篇分享文章制作是在windows上使用的nginx,一般正式发布的时候是在linux来配 ...

  3. 分布式锁 基于Redis

    分布式锁的实现(基于Redis) 参考:http://www.jb51.net/article/75439.htm http://www.linuxidc.com/Linux/2015-01/1118 ...

  4. 【分布式锁】Redis实现可重入的分布式锁

    一.前言 之前写的一篇文章<细说分布式锁>介绍了分布式锁的三种实现方式,但是Redis实现分布式锁关于Lua脚本实现.自定义分布式锁注解以及需要注意的问题都没描述.本文就是详细说明如何利用 ...

  5. 2流高手速成记(之八):基于Sentinel实现微服务体系下的限流与熔断

    我们接上回 上一篇中,我们进行了简要的微服务实现,也体会到了SpringCloudAlibaba的强大和神奇之处 我们仅改动了两个注释,其他全篇代码不变,原来的独立服务就被我们分为了provider和 ...

  6. Redis解读(4):Redis中HyperLongLog、布隆过滤器、限流、Geo、及Scan等进阶应用

    Redis中的HyperLogLog 一般我们评估一个网站的访问量,有几个主要的参数: pv,Page View,网页的浏览量 uv,User View,访问的用户 一般来说,pv 或者 uv 的统计 ...

  7. Rabbitmq之高级特性——实现消费端限流&NACK重回队列

    如果是高并发下,rabbitmq服务器上收到成千上万条消息,那么当打开消费端时,这些消息必定喷涌而来,导致消费端消费不过来甚至挂掉都有可能. 在非自动确认的模式下,可以采用限流模式,rabbitmq ...

  8. 分布式限流组件-基于Redis的注解支持的Ratelimiter

    原文:https://juejin.im/entry/5bd491c85188255ac2629bef?utm_source=coffeephp.com 在分布式领域,我们难免会遇到并发量突增,对后端 ...

  9. springboot + aop + Lua分布式限流的最佳实践

    整理了一些Java方面的架构.面试资料(微服务.集群.分布式.中间件等),有需要的小伙伴可以关注公众号[程序员内点事],无套路自行领取 一.什么是限流?为什么要限流? 不知道大家有没有做过帝都的地铁, ...

随机推荐

  1. 服务监控之 Spring Boot Admin.

    一.概述  开始阅读这篇文章之前,建议先阅读下<SpringBoot 之Actuator>,该篇文章提到 Spring Boot Actuator 提供了对单个Spring Boot的监控 ...

  2. 朝花夕拾《精通CSS》二、选择器 & 层叠

    一.背景 翻出我4年前看的<精通CSS>一书,可惜当初没有整理读书笔记的习惯,最近又很少写前端,遂很多东西.知识点遗忘了,恰且现在 css 也有些变化和进步,遂一起打包整理,输出成几篇 b ...

  3. Exe4j 打包: this executable was created with an evaluation version of exe4j

    异常 this executable was created with an evaluation version of exe4j   异常.png 问题原因 当前打包使用exe4j未授权 解决方法 ...

  4. JavaScript 引擎 V8 执行流程概述

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/t__Jqzg1rbTlsCHXKMwh6A作者:赖勇高 本文主要讲解的是V8的技术,是V8的入 ...

  5. redis 事务(悲观锁和乐观锁)

    MULTI 开启事务,后续的命令会被加入到同一个事务中 事务中的操作会发送给客服端,但是不会立即执行,而是将操作放到了该事务对应的一个队列中,服务端返回QUEQUD EXEC 执行EXEC后,事务中的 ...

  6. Django路由系统的简介与使用

    Django的路由系统 Django 1.11版本 URLConf官方文档 URL配置(URLconf)就像Django 所支撑网站的目录.它的本质是URL 与 为该URL调用的视图函数之间的映射表. ...

  7. Docker安装ELK

    1.下载elk docker pull sebp/elk 2.启动elk //Elasticsearch至少需要单独2G的内存 //增加了一个volume绑定,以免重启container以后ES的数据 ...

  8. jvm虚拟机笔记<四> 虚拟机字节码执行引擎

    一.运行时栈帧结构 栈帧是用于支持虚拟机进行方法调用和执行的数据结构,是虚拟机栈的栈元素. 栈帧存储了局部变量表,操作数栈,动态连接,和返回地址等. 每一个方法的执行 对应的一个栈帧在虚拟机里面从入栈 ...

  9. SAP MCH1表和MCHA表更新逻辑

    SAP MCH1表和MCHA表更新逻辑 笔者所在的A项目里,批次是在material level 唯一, 意味着不同物料号可以有相同的批次号,只要物料号+批次号组合是唯一的即可. SE11 看MCH1 ...

  10. jenkins构建找不到python依赖

    最近在搞jenkins自动构建,遇到这样一个问题,我装的python相关的依赖在X用户下,但是jenkins在root下运行的,所以找不到相关的import 1. 执行pip freeze,会打印相关 ...