个人使用向6:redis限流

场景:简单限流。起因是查看同事的博客有感而发。给自己一些压力,感兴趣的就去做。版本5天应该有一个初版,后续有时间可以再去优化。如果你第一个版本都没有那么你后面拿什么让别人知道你做了什么。至自己,从2020-12-11开始立这个写的念头都到现在,应该动手写些东西了。
2020-12-16来补全了。具体一些都在gitee中有,接下去展示细节,以及一些感悟吧。互相进步。
对应我参考创建了demo 对应上传gitee
https://gitee.com/zyiwei/redis-current-limiting

限流,限制某些接口的用户调用次数。原先的公司业务也有这样的需求,为什么?因为某些服务的返回值有商业价值。比如某些金融行业的返回值,或者是你提供一些东西的查询服务,比如汽车信息,租车网都带一些汽车基本信息的参考,比如平均价格是多少,这些带商业价值的信息,都会有人来频繁爬取调用。对于你来说,可能仅仅是本地数据库的数据查询,加上是本网站的注册人员,就认为是简单的单表增删改查,未引起注意。但是爬虫过来就是不讲武德,过来就是给你短时间内频繁调用该接口,都是同一个用户,同一个IP进行调用该接口,就很疯狂,10分钟5000次。就很影响其他用户的体验,如果你接口内部数据做其他处理,这个吞吐量还需要下去。那么限流就很有必要,避免这样不讲武德的来影响其他用户体验。

实现方法:限流用redis + lua 来实现。也有很多其他不同思路。但是大致是针对这么几个大致思路:比如给用户访问某个接口必要的身份识别,就是流量桶的概念,我对于这个接口就开放了对应时间内有效的访问次数,你们谁访问,就过来拿身份识别,才能访问。另外就是你记录登记访问次数,好家伙就是你小子频繁过来,我就不让你进了。其中redis+lua限流大致就是这种登记你访问次数,到达一定次数之后,我就不让你进行访问了。

关于最开始的在创业公司那份工作也有想过对应问题。但是给的想法是 利用redis的自增进行,然后调用接口对该自增值进行判断,如果是到了某个值就进行拒绝。
这里也是简单限流因为,还是局限单机,并不是一来就多并发。

开始正式搞事:具体代码在gitee都有。以下是部分接口。
maven配置

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>21.0</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<!--集成redis-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-redis</artifactId>
			<version>1.4.7.RELEASE</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.72</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>

对应配置文件配置

spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=30000

配置RedisTemplate实例

/**  2020-12-15  个人理解属于 你输入的跟redis存储时候的转换用 一种类似json的方式存储起来 */
@Configuration
public class RedisLimiterHelper {

    @Bean
    public RedisTemplate<String, Serializable> limitRedisTemplate(org.springframework.data.redis.connection.RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> template = new RedisTemplate<>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}

限流枚举类

public enum  LimitType  {

    /**
     * 自定义key
     */
    CUSTOMER,

    /**
     * 请求者IP  -- 通常是某个IP  不过会有人进行虚拟IP进行访问
     */
    IP,

    /** 下面不是非正常的,但是是一个不同维度去限制某些限流的方法  一般来说 按照用户ID 进行限流,然后是根据IP进行限流 ,
     * 再过分一些就是访问某个接口,对于接口进行限流,当某个接口被调用多少次之后,就在一段时间内进行自我限制。防止某些接口被一瞬间就冲垮了 */
    /**
     * 客户ID 一般来说某个用户进来访问,某个接口,应该是有对应用户ID,以及一些必要信息
     */
    CUSTOMERID,

    /**
     * 浏览器  用户访问有习惯的,但是一般是要求基于同一个浏览器,当然这个似乎不在本次考虑的访问内。一般是没有具体浏览器,认为是用其他访问。
     * 不过对应可以是一个思路
     */
    BROWSER,

    /**
     * 某个表的ID
     */
    XXID;
}

然后搞点 AOP 有关系的
自定义注解

/**
 * @author fuhua
 * @description 自定义限流注解
 * @date 2020/12/12 13:15
 * 2020-12-15 理解  这里是对应的一些值注解。这部分参考网上的AOP的创建,对应资料就比较多了
 * 跟枚举类
 * @see com.fuhua.demoXL.Enum.LimitType  存在联动
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Limit {

    /**
     * 名字
     */
    String name() default "";

    /**
     * key
     */
    String key() default "";

    /**
     * Key的前缀
     */
    String prefix() default "";

    /**
     * 给定的时间范围 单位(秒)
     */
    int period();

    /**
     * 一定时间内最多访问次数
     */
    int count();

    /**
     * 限流的类型(用户自定义key 或者 请求ip)
     */
    LimitType limitType() default LimitType.CUSTOMER;

    /**
     * 限流后返回描述 可以是准确提示:比如 某个IP,对应访问次数超过了XX次数;如果对外,就说繁忙,稍后再试,委婉拒绝就可以了
     */
    String describle() default "当前系统繁忙,请稍后再试";
}

切面实现

/**
 * @author fuhua
 * @description 限流切面实现
 * @date 2020/4/8 13:04
 */
@Aspect
@Configuration
public class LimitInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);

    private static final String UNKNOWN = "unknown";

    private final RedisTemplate<String, Serializable> limitRedisTemplate;

    @Autowired
    public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
        this.limitRedisTemplate = limitRedisTemplate;
    }

    /** 个人觉得比较好的写法  */
    //截获标有@CacheRemove的方法
    @Pointcut(value = "(execution(public * *(..)) && @annotation(com.fuhua.demoXL.Interface.Limit))")
    private void pointcut() {
    }

    /**
     * @param pjp
     * @author fuhua
     * @description 切面
     * @date 2020/12/12 13:15
     *
     * 传入参数 ProceedingJoinPoint  环绕通知 ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行
     *  Proceedingjoinpoint 继承了 JoinPoint 。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
     *
     *  参考网址 https://www.cnblogs.com/zhjh256/p/10694165.html  以后有时间 等整个专场,好好品品
     */
    @Around("pointcut()")
    public Object interceptor(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        LimitType limitType = limitAnnotation.limitType();
        String name = limitAnnotation.name();
        String key;
        int limitPeriod = limitAnnotation.period();
        int limitCount = limitAnnotation.count();

        /**
         * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key
         */
        switch (limitType) {
            /** 可扩展 因为我们之前拓展了 Limit */
            case IP:
                key = getIpAddress();
                break;
            case CUSTOMER:
                key = limitAnnotation.key();
                break;
            default:
                key = StringUtils.upperCase(method.getName());
        }

        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
        try {
            /** 对应lua语句  轻量小巧的脚本语言 目的为了嵌入应用程序中  对应lua语法进行简单解析了*/
            String luaScript = buildLuaScript();
            /** 这里可能理解不太到位  对应应该是将lua转换成RedisScript 类型  然后后续可以进行执行 */
            RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
            /** 对应keys   以及 limitCount跟limitPeriod  是 Object... args */
            Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
            /** 后续是正常的数据 正常的java逻辑就可以进行梳理 */
            logger.info("Access try count is {} for name={} and key = {}", count, name, key);
            if (count != null && count.intValue() <= limitCount) {
                return pjp.proceed();
            } else {
                throw new RuntimeException("You have been dragged into the blacklist");
            }
        } catch (Throwable e) {
            if (e instanceof RuntimeException) {
                throw new RuntimeException(e.getLocalizedMessage());
            }
            throw new RuntimeException("server exception");
        }
    }

    /**
     * @author fu
     * @description 编写 redis Lua 限流脚本  对应执行lua 对应包是在 org.springframework.data.redis
     *
     * 首先 lua语言比较像 C 所以习惯起来还是不是很难的
     * local c   创建一个C的局部变量  变量应该是local ,不可以是全局变量,否则会报错
     * \n 是回车  c = redis.call('get',KEYS[1])   这里主要是理解 KEYS[1] 跟 redis.call()
     * redis.call()  是Lua脚本调用Redis的Lua函数  另外一个是 redis.pcall()
     * 二个区别是出错,redis.call()返回调用者一个错误;redis.pcall()  对应错误以Lua表的形式返回
     * KEYS[1] 用来表示在redis中用作键值的参数占位,主要是用来传递redis中用作keys值的参数
     * if c and tonumber(c) > tonumber(ARGV[1]) then   【Lua 中 0为true】 【ARGV[1]是返回值的VALUE值】
     * return c;  返回对应值
     * end  结束语
     * c = redis.call('incr',KEYS[1])  同理 对应是自增的KEYS 1
     * if tonumber(c) == 1 then   //tonumber 对应是转换成数字类型的,因为redis出来的可能是String类型
     * redis.call('expire',KEYS[1],ARGV[2])  对应放置过期时间对应某个值放第二个对应时间的值
     * end
     * return c;
     * @date 2020/4/8 13:24
     */
    public String buildLuaScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c");
        lua.append("\nc = redis.call('get',KEYS[1])");
        // 调用不超过最大值,则直接返回
        lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
        lua.append("\nreturn c;");
        lua.append("\nend");
        // 执行计算器自加
        lua.append("\nc = redis.call('incr',KEYS[1])");
        lua.append("\nif tonumber(c) == 1 then");
        // 从第一次调用开始限流,设置对应键值的过期
        lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
        lua.append("\nend");
        lua.append("\nreturn c;");
        return lua.toString();
    }


    /**
     * @author fuhua
     * @description 获取id地址  获取IP 不过对应如果是通过NGIX的话 还需要做一些转换。以及有一些会跳转多次,要还原出最初调用的IP
     * @date 2020/12/15 13:24
     */
    public String getIpAddress() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        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();
        }
        return ip;
    }
}

控制层就是单纯参考开源的前辈
我们将@Limit注解作用在需要进行限流的接口方法上,下边我们给方法设置@Limit注解,在10秒内只允许放行3个请求,这里为直观一点用AtomicInteger计数。

/**
 * @Author: fu
 * @Description:
 */
@RestController
public class LimiterController {

    private static final AtomicInteger ATOMIC_INTEGER_1 = new AtomicInteger();
    private static final AtomicInteger ATOMIC_INTEGER_2 = new AtomicInteger();
    private static final AtomicInteger ATOMIC_INTEGER_3 = new AtomicInteger();

    /**
     * @author fu
     * @description
     * @date 2020/4/8 13:42
     */
    @Limit(key = "limitTest", period = 10, count = 3)
    @GetMapping("/limitTest1")
    public int testLimiter1() {

        return ATOMIC_INTEGER_1.incrementAndGet();
    }

    /**
     * @author fu
     * @description
     * @date 2020/4/8 13:42
     */
    @Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER)
    @GetMapping("/limitTest2")
    public int testLimiter2() {

        return ATOMIC_INTEGER_2.incrementAndGet();
    }

    /**
     * @author fu
     * @description
     * @date 2020/4/8 13:42
     */
    @Limit(key = "ip_limit_test", period = 10, count = 3, limitType = LimitType.IP)
    @GetMapping("/limitTest3")
    public int testLimiter3() {

        return ATOMIC_INTEGER_3.incrementAndGet();
    }

}

测试效果
测试「预期」:连续请求3次均可以成功,第4次请求被拒绝。接下来看一下是不是我们预期的效果,请求地址:http://127.0.0.1:8080/limitTest1,用postman进行测试,有没有postman url直接贴浏览器也是一样。
在这里插入图片描述

参考的网址为:
https://blog.youkuaiyun.com/Wzy000001/article/details/109855461

https://mp.weixin.qq.com/s/kyFAWH3mVNJvurQDt4vchA

初版要出。正版在这星期逼自己完成 ,加油吧
正版2020-12-16补完。demo放在对应gitee上面。学习知识点点尽量整个demo。没有什么原因,能跑起来的才是可以使用的,或许对于一些大佬来说,思维点拨很关键,具体代码需要靠你自己。但是我还是觉得demo能跑起来,至少能让人很有信心走下去,不过CV救不了程序员,还是要理解跟迁移到自己的项目中。跑demo从来不是一个难事,但是整合就会出现很多问题。因为该部分是参考同事的,但是我对于一些我觉得可以的部分进行我自己的思考,哪怕后期让我去改代码,都没在怕的。回顾这个过程。从选题到动手,然后编写demo,完善demo理解,最后进行文章编写。如果按完整时间来算,编写demo,大概30分钟,完善的话20分钟百度,查找一些跟自己的知识认知池进行对接。写的话涂涂改改,大概2小时吧。起始只要你动手,东西推动进度就不会慢。要逼自己动手啊。


漫漫长路,一个小周跟他一个小陈朋友一起努力奔跑。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值