分布式限流解决方案-Redis+Lua

分布式限流解决方案-Redis+Lua

1、分析

黑客或者一些恶意的用户为了攻击网站或者APP,通过并发用肉机并发或者死循环请求接口,从而导致系统出现宕机。

  • 针对新增数据的接口,会出现大量的重复数据,甚至垃圾数据会将数据库和CPU或者内存磁盘耗尽,直到数据库撑爆为止。
  • 针对查询的接口。一般是重点攻击慢查询,比如一个SQL是2S。只要一致攻击,就必然造成系统被拖垮,数据库查询全都被阻塞,连接一直得不到释放造成数据库无法访问。

一个用户在1秒钟之内,只允许请求n次。

2、Redis + Lua实现限流解决方案

2.1 redis依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<!--这里就是redis的核心jar包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.2 限流userLimit.lua脚本
-- 限流
-- 获取方法签名特征
local key = KEYS[1]
-- 调用脚本传入的限流大小
local limit = tonumber(ARGV[1])
-- 获取当前流量大小
local count = tonumber(redis.call('get',key) or "0")
-- 是否超出限流阈值
if count + 1 > limit then
    -- 拒绝服务
    return false
else
    -- 没有超过阈值
    -- 设置当前访问的数量 + 1
    redis.call("incr",key)
    -- 设置过期时间
    redis.call("expire",key,ARGV[2])
    return true
end
3.3 加载lua脚本的DefaultRedisScript对象
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

/**
 * @Auther: 长颈鹿
 * @Date: 2021/08/16/12:48
 * @Description: 将lua脚本的内容加载出来放入到DefaultRedisScript
 */
@Configuration
public class LuaConfiguration {
    
    @Bean
    public DefaultRedisScript<Boolean> limitUserAccessLua() {
        // 初始化一个lua脚本的对象DefaultRedisScript
        DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
        // 通过这个对象去加载lua脚本的位置 ClassPathResource读取类路径下的lua脚本
        // ClassPathResource 什么是类路径:就是你maven编译好的target/classes目录
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/userLimit.lua")));
        // lua脚本最终的返回值 建议数字返回。1/0
        defaultRedisScript.setResultType(Boolean.class);
        return defaultRedisScript;
    }

}
2.4 改写RedisTemplate规则
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @Auther: 长颈鹿
 * @Date: 2021/08/16/12:50
 * @Description:
 */
@Configuration
public class RedisConfiguration {

    /**
     * 改写redisTemplate序列化规则
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 创建redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 开始redis连接工厂跪安了
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 创建json序列化方式
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置key用string序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置value用jackjson进行处理
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash也要进行修改
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        // 默认调用
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

}
2.5 StringRedisTemplate调用和执行lua
package com.example.controller;

import com.example.limit.annotation.AccessLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * @Auther: 长颈鹿
 * @Date: 2021/08/16/12:53
 * @Description:
 */
@RestController
public class RateLimiterController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private DefaultRedisScript<Boolean> limitUserAccessLua;

    @GetMapping("/limit/user")
    public String limitUser(String userid) {
        // 定义key是的列表
        List<String> keysList = new ArrayList<>();
        keysList.add("user:"+userid);
        // 执行执行lua脚本限流
        Boolean accessFlag = stringRedisTemplate.execute(limitUserAccessLua, keysList, "1","1");
        // 判断当前执行的结果,如果是0,被限制,1代表正常
        if (!accessFlag) {
            throw new RuntimeException("server is busy!!!");
        }
        return "success";
    }

}

3、注解版本限流解决方案

3.1 定义aop依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 定义AccessLimiterAspect切面类
package com.example.limit.aop;

import com.example.limit.annotation.AccessLimiter;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Auther: 长颈鹿
 * @Date: 2021/08/16/12:58
 * @Description:
 */
@Aspect
@Component
public class AccessLimiterAspect {

    private static final Logger log = LoggerFactory.getLogger(AccessLimiterAspect.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private DefaultRedisScript<Boolean> limitUserAccessLua;

    // 切入点
    @Pointcut("@annotation(com.example.limit.annotation.AccessLimiter)")
    public void cut() {
        System.out.println("cut");
    }

    // 通知和连接点
    @Before("cut()")
    public void before(JoinPoint joinPoint) throws Exception {

        // 获取到执行的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 通过方法获取到注解
        AccessLimiter annotation = method.getAnnotation(AccessLimiter.class);
        // 如果 annotation==null,说明方法上没加限流AccessLimiter,说明不需要限流操作
        if (annotation == null) {
            return;
        }
        // 获取到对应的注解参数
        String key = annotation.key();
        Integer limit = annotation.limit();
        Integer timeout = annotation.timeout();

        // 如果key为空
        if (StringUtils.isEmpty(key)) {
            String name = method.getDeclaringClass().getName();
            // 直接把当前的方法名给与key
            key = name+"#"+method.getName();
            // 获取方法中的参数列表

            //ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
            //String[] parameterNames = pnd.getParameterNames(method);

            Class<?>[] parameterTypes = method.getParameterTypes();
            for (Class<?> parameterType : parameterTypes) {
                System.out.println(parameterType);
            }

            // 如果方法有参数,那么就把key规则 = 方法名“#”参数类型
            if (parameterTypes != null) {
                String paramTypes = Arrays.stream(parameterTypes)
                        .map(Class::getName)
                        .collect(Collectors.joining(","));
                key = key +"#" + paramTypes;
            }
        }

        // 定义key是的列表
        List<String> keysList = new ArrayList<>();
        keysList.add(key);
        // 执行执行lua脚本限流
        Boolean accessFlag = stringRedisTemplate.execute(limitUserAccessLua, keysList, limit.toString(), timeout.toString());
        // 判断当前执行的结果,如果是0,被限制,1代表正常
        if (!accessFlag) {
            throw new Exception("server is busy!!!");
        }
    }

}
3.3 定义AccessLimiter注解
package com.example.limit.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {
    // 目标: @AccessLimiter(limit="1",timeout="1",key="user:ip:limit")
    // 解读:一个用户key在timeout时间内,最多访问limit次
    // 缓存的key
    String key() default "";
    // 限制的次数
    int limit() default  1;
    // 过期时间
    int timeout() default  1;
}
3.4 限流测试
@GetMapping("/limit/aop/user")
@AccessLimiter(limit = 1, timeout = 1)
public String limitAopUser(String userId) {
    return "success";
}


@GetMapping("/limit/aop/user3")
@AccessLimiter(limit = 10, timeout = 1)
public String limitAopUse3(String userId) {
    return "success";
}

@GetMapping("/limit/aop/user2")
public String limitAopUser2(String userId) {
    return "success";
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南宫拾壹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值