【分布式架构】(10)---基于Redis组件的特性,实现一个分布式限流

分布式---基于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/ipLimiter

参考

这个设计是我在刷github的时候看到确实很好,我这边只是在它的基础上做了点小改动,非常感谢该作者的分享。
github地址:https://github.com/TaXueWWL/shleld-ratelimter

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

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

原文地址:https://www.cnblogs.com/qdhxhz/p/10982218.html

时间: 2024-10-28 11:36:39

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

springboot + aop + Lua分布式限流原理解析

一.什么是限流?为什么要限流?不知道大家有没有做过帝都的地铁,就是进地铁站都要排队的那种,为什么要这样摆长龙转圈圈?答案就是为了 限流 !因为一趟地铁的运力是有限的,一下挤进去太多人会造成站台的拥挤.列车的超载,存在一定的安全隐患.同理,我们的程序也是一样,它处理请求的能力也是有限的,一旦请求多到超出它的处理极限就会崩溃.为了不出现最坏的崩溃情况,只能耽误一下大家进站的时间. 限流是保证系统高可用的重要手段!!! 由于互联网公司的流量巨大,系统上线会做一个流量峰值的评估,尤其是像各种秒杀促销活动

Java 实现基于Redis的分布式可重入锁

如何实现可重入? 首先锁信息(指redis中lockKey关联的value值) 必须得设计的能负载更多信息,之前non-reentrant时value直接就是一个超时时间,但是要实现可重入单超时时间是不够的,必须要标识锁是被谁持有的,也就是说要标识分布式环境中的线程,还要记录锁被入了多少次. 如何在分布式线程中标识唯一线程? MAC地址 +jvm进程 + 线程ID(或者线程地址都行),三者结合即可唯一分布式环境中的线程.下载 实现 锁的信息采用json存储,格式如下: 代码框架还是和之前实现的非

Java实现基于Redis的分布式锁

我这里不实现JDK的java.util.concurrent.locks.Lock接口,而是自定义一个,因为JDK的有个newCondition()方法我这里暂时没实现.这个Lock提供了5个lock方法的变体,可以自行选择使用哪一个来获取锁,我的想法是 最好用带超时返回的那几个方法,因为不这样的话,假如redis挂了,线程永远都在那死循环了(关于这里,应该还可以进一步优化,如果redis挂了,Jedis的操作肯定会抛异常之类的,可以定义个机制让redis挂了的时候通知使用这个lock的用户,或

基于redis的分布式锁实现

关于分布式锁 很久之前有讲过并发编程中的锁并发编程的锁机制:synchronized和lock.在单进程的系统中,当存在多个线程可以同时改变某个变量时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量.而同步的本质是通过锁来实现的.为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记.

一文归纳总结分布式架构的那些事!

双十一购物狂欢节已经过去了,作为程序员的你,除了在关心自己的快递的运送情况之外,最关心的是不是双十一架构技术是如何承受亿级用户流量的冲击,又是如何在分布式架构中实现单点登陆,形成支持高并发,高可用的分布式架构技术呢?下面小编就来帮你总结如何从0到1学习分布式架构技术,如何实现从小白到架构师的蜕变!! 一.初识分布式架构: 1.初识分布式架构及意义 2.如何把应用从单机架构扩展到分布式 3.大型分布式架构的演进过程 4.构建分布式架构的重要因素 (1)CDN加速静态访问 (2)分布式储存 (3)分

分布式架构的前世今生...

一.前言 ? 随着社会的发展,技术的进步,以前的大型机架构很显然由于高成本.难维护等原因渐渐地变得不再那么主流了,替代它的就是当下最火的分布式架构,从大型机到分布式,经历了好几个阶段,我们弄明白各个阶段的架构,才能更好地理解和体会分布式架构的好处,那么本文我们就来聊聊分布式架构的演进过程,希望能给大家带来眼前一亮的感觉. 二.背景说明 ? 我们都知道一个成熟的大型网站的系统架构并非一开始就设计的非常完美,也没有一开始就具备高性能.高并发.高可用.安全性等特性,而是随着用户量的增加.业务功能的扩展

阿里P8架构师告诉你什么是分布式架构

一.前言 我们都知道,当今无论在BAT这样的大公司,还是各种各样的小公司,甚至是传统行业刚转互联网的企业都开始使用分布式架构,那么什么叫分布式架构呢?分布式架构有什么好处呢?分布式架构经过了怎样的发展呢?是哪家企业开启了分布式架构的时代呢?读完本文,你就会得到这些答案,下面让我们一起来开启分布式概述的奇妙之旅吧! 二.分布式架构的发展历史 1946年2.14日,那是一个浪漫的情人节 , 世界上第一台电子数字计算机在美国宾夕法尼亚大学诞生了,她的名字叫ENIAC.这台计算机占地170平米.重达 3

分布式架构下,session共享有什么方案么?

分布式架构下,session共享有什么方案么? 会点代码的大叔 科技领域创作者 分布式架构下的session共享,也可以称作分布式session一致性:关于这个问题,和大家说一说解决方案(如果有其他的方案,可以留言讨论). session的作用 如果大家做过web应用开发的话,应该对session比较熟悉:服务器会为每个用户创建一个会话,存储用户的相关信息,以便在后面的请求中,可以够定位到同一个上下文. 例如用户在登录之后,再进行页面跳转的时候,存储在session中的信息会一直保持,如果用户还

分布式架构的总结

一.前言 ? 随着社会的发展,技术的进步,以前的大型机架构很显然由于高成本.难维护等原因渐渐地变得不再那么主流了,替代它的就是当下最火的分布式架构,从大型机到分布式,经历了好几个阶段,我们弄明白各个阶段的架构,才能更好地理解和体会分布式架构的好处,那么本文我们就来聊聊分布式架构的演进过程,希望能给大家带来眼前一亮的感觉. 二.背景说明 ? 我们都知道一个成熟的大型网站的系统架构并非一开始就设计的非常完美,也没有一开始就具备高性能.高并发.高可用.安全性等特性,而是随着用户量的增加.业务功能的扩展