redis + lua脚本 + AOP实现接口对客户端限流

 接口每秒限制客户端访问200次

基于应用层限流,目的是:防止用户恶意刷新接口,保证程序稳定,高可用

Lua优点:
减少网络开销:这个脚本只要执行一次,能减少网络传输
原子性:Redis将这个脚本作为原子执行要么全部成功或者失败,不担心并发问题,不需要事务,(PS:LUA脚本保证原子性,执行lua脚本时不会同时执行其它脚本或redis命令, 这种语义类似于MULTI/EXEC,这个lua脚本要么执行成功,要么执行失败
复用性:lua一旦执行就能永久性保存Redis的数据,可以供其它客户端使用 

创建lua,放到resource下myLimit_ip.lua 

-- 为某个接口的请求IP设置计数器,比如:127.0.0.1请求查询用户接口
-- KEYS[1] = 127.0.0.1 也就是用户的IP
-- ARGV[1] = 过期时间 1s
-- ARGV[2] = 限制的次数
local count = redis.call('incr',KEYS[1]);
if count == 1 then
    redis.call("expire",KEYS[1],ARGV[2])
end
-- 如果时间还没有过期,并且还在规定的次数内,则请求同一接口
if count > tonumber(ARGV[1]) then
    return false
end

return true
MyLuaConfiguration读取lua
@Configuration
public class MyLuaConfiguration {

    /** 
     * 将lua脚本的内容加载出来放入到DefaultRedisScript
     * @author fan 
     * @date 2022/5/7 2:35 
     * @return org.springframework.data.redis.core.script.DefaultRedisScript<java.lang.Boolean> 
    */
    @Bean
    public DefaultRedisScript<Boolean> ipLimitLua() {
        DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("myLimit_ip.lua")));
        defaultRedisScript.setResultType(Boolean.class);
        return defaultRedisScript;
    }
}
 
MyAcessLimter
/**创建自定义的限流注解在Controller使用
 * @author fan
 * @date 2022年05月07日 1:08
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAcessLimter {

    

    /**
     * 每timeout限制请求的个数
     * @author fan
     * @date 2022/5/7 1:54
     * @return int
    */
    int count() default 5;

    /**
     * 超时时间,单位默认是秒
     * @author fan
     * @date 2022/5/7 1:54
     * @return int
    */
    int timeout() default 10;

    /**
     * 访问间隔
     * @author fan
     * @date 2022/5/7 1:54
     * @return int
    */
    int waits () default 20;
}
MyAcessLimiterAspect
/**
 * @author fan
 * @date 2022年05月07日 1:07
 */
@Component
@Aspect
@Slf4j
public class MyAcessLimiterAspect {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private DefaultRedisScript<Boolean> myClientIPLimitLua;

    // 1: 切入点   创建的注解类
    @Pointcut("@annotation(com.fan.li.limit.MyAcessLimter)")
    public void myLimiterPonicut() {
    }

    @Before("myLimiterPonicut()")
    public void limiter(JoinPoint joinPoint) {
        log.info("限流开始......." + LocalDate.now());
        
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String classname = methodSignature.getMethod().getDeclaringClass().getName();
        String packageName = methodSignature.getMethod().getDeclaringClass().getPackage().getName();
        log.info("method:{},classname:{},packageName:{}",method,classname,packageName);
       
        MyAcessLimter annotation = method.getAnnotation(MyAcessLimter.class);
        
        String methodNameKey = method.getName();
        log.info("获取注解方法名:{}",methodNameKey);
        
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();
        String userClientIp = MyClientIPUtils.getIpAddr(request);
        log.info("当前用户IP:{}", userClientIp );
        
        Integer count = annotation.count();
        Integer timeout = annotation.timeout();

        /*Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            log.info("参数id--> " + request.getParameter("userId") + "---" + args[i]);
        }*/
        String key = request.getParameter("userId") + ":" + userClientIp ;//这里用userId + userClientIp 作为key
        log.info("当前的key-->" + key);
        
        Boolean b =  stringRedisTemplate.execute(myClientIPLimitLua, Lists.newArrayList(key), count.toString(), timeout.toString());//读取lua
        
        if (!b) {
            
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html;charset=UTF-8");
            try (PrintWriter writer = response.getWriter();) {
                writer.print("<h1>操作频繁,请稍后在试</h1>");
            } catch (Exception ex) {
                throw new RuntimeException("操作频繁,请稍后在试");
            }
        }
    }
    
}

MyClientIPUtils

/**获取真实访问的IP
 * @author fan
 * @date 2022年05月07日 1:06
 */
public class MyClientIPUtils{

    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        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("X-Forwarded-For");
        }
        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.getHeader("X-Real-IP");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}
MyLimiterController
/**
 * 描述: 测试限流
 *
 * @author fan
 * @create 2022年05月07日 1:59
 **/
@RestController
@RequestMapping(value = "limit")
public class MyLimiterController {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private UserService userService;


    /**
     * 接口每秒限制访问200次
     * @author fan
     * @date 2022/5/7 2:00
     * @return list
    */
    @GetMapping("/index")
    @MyAcessLimter(count = 200,timeout = 1)
    public List<User> index(String userId) {//对需要做限流的接口
        List<User> list =  userService.selectUserById(Integer.parseInt(userId));
        //list.get(0);
        return list;
    }
}

效果图:把次数200改成5测试

1S内操作次数超过5次就会提示: 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis 中创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本实现漏桶算法。 具体实现步骤如下: - 在 Redis 中创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值