基于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


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秒后才可以重新访问,这样反反复复。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot结合Redis实现接口限的步骤如下[^1][^2]: 1. 引入依赖:在Spring Boot项目的pom.xml文件中添加Redis和AOP的相关依赖。 2. 配置RedisTemplate:在Spring Boot的配置文件中配置Redis连接信息,包括主机名、端口号、密码等。 3. 创建自定义注解:使用@RateLimiter注解来标记需要进行接口限流的方法。 4. 编写切面类:创建一个切面类,使用@Aspect注解标记,并在该类中编写切点和通知方法。 5. 实现接口限流逻辑:在通知方法中,使用Redis的原子操作来实现接口限流的逻辑。可以使用Redis的incr命令来对接口的访问次数进行计数,然后根据设定的阈值来判断是否限流。 6. 配置切面:在Spring Boot的配置类中,使用@EnableAspectJAutoProxy注解开启AOP功能,并将切面类添加到容器中。 7. 在需要进行接口限流的方法上添加注解:在需要进行接口限流的方法上添加@RateLimiter注解,并配置相关参数,如限流的阈值、时间窗口大小等。 8. 测试接口限流效果:启动Spring Boot应用程序,并访问被限流接口,观察接口的访问频率是否受到限制。 以下是一个示例代码,演示了如何使用Spring Boot和Redis实现接口限流: ```java // 1. 创建自定义注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimiter { int value() default 10; // 默认限流阈值为10 int window() default 60; // 默认时间窗口为60秒 } // 2. 编写切面类 @Aspect @Component public class RateLimiterAspect { @Autowired private RedisTemplate<String, String> redisTemplate; @Around("@annotation(rateLimiter)") public Object around(ProceedingJoinPoint joinPoint, RateLimiter rateLimiter) throws Throwable { String methodName = joinPoint.getSignature().getName(); String key = "rate_limiter:" + methodName; int limit = rateLimiter.value(); int window = rateLimiter.window(); // 使用Redis的incr命令对接口的访问次数进行计数 Long count = redisTemplate.opsForValue().increment(key, 1); if (count == 1) { // 设置过期时间,保证计数器在一定时间后自动清零 redisTemplate.expire(key, window, TimeUnit.SECONDS); } if (count > limit) { // 超过限流阈值,抛出异常或返回错误信息 throw new RuntimeException("接口访问频率超过限制"); } // 执行原方法 return joinPoint.proceed(); } } // 3. 在需要进行接口限流的方法上添加注解 @RestController public class DemoController { @RateLimiter(value = 5, window = 60) // 每分钟最多访问5次 @GetMapping("/demo") public String demo() { return "Hello World!"; } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值