通过滑动窗口实现接口调用的多种限制策略

· 浏览次数 : 8

小编点评

**生成内容的步骤:** 1. **获取Redis连接:**使用Redisson创建一个连接到Redis服务器的客户端。 2. **加载lua脚本:**加载包含执行lua脚本的Redisson脚本。 3. **执行lua脚本:**使用`eval`方法执行lua脚本。 4. **返回值结果:**根据脚本的返回值,设置结果。 **代码示例:** ```java import org.redisson.Redisson; import org.redisson.api.RScript; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedisLuaScriptExample { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); // Lua 脚本 String luaScript = "local key = KEYS[1]" + "local limit1 = tonumber(ARGV[1])" + "local limit2 = tonumber(ARGV[2])" + "local windowStart = tonumber(ARGV[3])" + "local currentTime = tonumber(ARGV[4])" + "redis.call('zremrangebyscore', key, 0, windowStart)" + "local currentSize = tonumber(redis.call('zcard', key))" + "if currentSize >= limit2 then" + " return 0" + "end" + "local oldestTimestamp = tonumber(redis.call('zrange', key, -1, -1, 'WITHSCORES')[2])" + "if (currentTime - oldestTimestamp) < 60000 then" + " return 0" + "end" + "if currentSize < limit1 then" + " redis.call('zadd', key, currentTime, currentTime)" + " return 1" + "else" + " return 0" + "end"; // 执行 lua脚本 RScript script = redisson.getScript(); // 执行脚本并获取结果 Long result = script.eval(RScript.Mode.READ_WRITE, luaScript, RScript.ReturnType.INTEGER, "your_key", // 替换成你的键 "5", // 替换成 limit1 的值 "10", // 替换成 limit2 的值 String.valueOf(System.currentTimeMillis() - 86400000), // 24小时前的时间戳 String.valueOf(System.currentTimeMillis())); // 打印结果 System.out.println("Result: " + result); redisson.shutdown(); } } ``` **注意:** * `your_key`应该替换成你的Redis键。 * `limit1`、`limit2`和`windowStart`的默认值应根据你的实际情况进行调整。 * 此代码仅供参考,你可以根据你的需求修改它。

正文

前言

有个邮箱发送的限制发送次数需求,为了防止用户恶意请求发送邮件的接口,对用户的发送邮件次数进行限制,每个邮箱60s内只能接收一次邮件,每个小时只能接收五次邮件,24小时只能接收十次邮件,一共有三个条件的限制。

实现方案

单机方案

单机简单实现可以用Caffeine,在Caffeine里面Key为mail的标识,value是个存这个mail每次接收邮件的时间戳List,数据结构如下图所示:

image-20240527165212319

  1. list小于5个:每一次有新元素入队,都要判断队列里最新的时间戳和当前时间戳是否超过60s,不超过返回60s限制。
  2. 大于等于5个,小于10,则当前队列size-5,即往前数第五个值,取对应的value时间戳,判断和当前时间超不超1h,超过就放入list,不超就返回超过一小时的限制。
  3. 如果数量等于10个,得先判断24小时超不超10个,拿List里面的第一个值,判断和当前的时间戳是否超过24小时,不超则返回24小时限制,超再判断1小数超不超,判断逻辑往前数五个,如果超过,则把第一个值剔除(即最老的那个元素),加入新的元素。

通过上面的数据结构,其实也能把剩余多少时间接触限制一并返回到前端,在达到限制的时候,对比时间戳时间的差距即可。

caffeine单机方案代码

 public boolean isMailCanSend(String mail){
        // 先判断缓存是否存在 不存在 则创建
        ArrayList<Long> mailTimeStampList = caffeineTemplate.getMailTimeStampFromCache(CacheKeyConstant.TIME_STAMP_LIST_FOR_MAIL + mail);
        if (mailTimeStampList == null) {
            ArrayList<Long> timeList = new ArrayList<>();
            timeList.add(System.currentTimeMillis());
            caffeineTemplate.addToMailTimeStampCache(CacheKeyConstant.TIME_STAMP_LIST_FOR_MAIL + mail, timeList);
            return true;
        } else {
            // 缓存存在
            // 存在先查60s
            Long timeStamp = mailTimeStampList.get(mailTimeStampList.size() - 1);
            // 判断与当前时间相差是否超过60s
            if (System.currentTimeMillis() - timeStamp > 60000) {
                // 再查数量是否小于5,满足直接加入缓存
                if (mailTimeStampList.size() < 5) {
                    mailTimeStampList.add(System.currentTimeMillis());
                    caffeineTemplate.addToMailTimeStampCache(CacheKeyConstant.TIME_STAMP_LIST_FOR_MAIL + mail, mailTimeStampList);
                    return true;
                } else {
                    // 大于等于5数量小于10
                    if (mailTimeStampList.size() < 10) {
                        // 则判断前面第五个是否满足一个小时
                        if (System.currentTimeMillis() - mailTimeStampList.get(mailTimeStampList.size() - 5) < 3600000) {
                            // 不满足大于一个小时 则不可发送
                            throw new EmailException(ResultCodeEnum.MAIL_ONE_HOUR_REQUEST_FREQUENT_ERROR, 3600000L - (System.currentTimeMillis() - mailTimeStampList.get(mailTimeStampList.size() - 5)));
                        } else {
                            mailTimeStampList.add(System.currentTimeMillis());
                            caffeineTemplate.addToMailTimeStampCache(CacheKeyConstant.TIME_STAMP_LIST_FOR_MAIL + mail, mailTimeStampList);
                            return true;
                        }
                    } else {
                        // 数量为 10的时候
                        // 等于10 判断大于24小时是否满足
                        if (System.currentTimeMillis() - mailTimeStampList.get(0) > 86400000) {
                            // 则判断前面第五个是否满足一个小时
                            if (System.currentTimeMillis() - mailTimeStampList.get(mailTimeStampList.size() - 5) < 3600000) {
                                // 不满足一个小时 则不可发送
                                throw new EmailException(ResultCodeEnum.MAIL_ONE_HOUR_REQUEST_FREQUENT_ERROR, 3600000L - (System.currentTimeMillis() - mailTimeStampList.get(mailTimeStampList.size() - 5)));
                            } else {
                                // 移除第一个
                                mailTimeStampList.remove(0);
                                mailTimeStampList.add(System.currentTimeMillis());
                                caffeineTemplate.addToMailTimeStampCache(CacheKeyConstant.TIME_STAMP_LIST_FOR_MAIL + mail, mailTimeStampList);
                                return true;
                            }
                        } else {
                            throw new EmailException(ResultCodeEnum.MAIL_24_HOUR_REQUEST_FREQUENT_ERROR, 86400000L - (System.currentTimeMillis() - mailTimeStampList.get(0)));
                        }
                    }
                }
            } else {
                throw new EmailException(ResultCodeEnum.MAIL_ONE_MIN_REQUEST_FREQUENT_ERROR, 60000L - (System.currentTimeMillis() - timeStamp));
            }


        }

分布式方案

分布式方案可以使用redis的zset数据结构来实现,同样是维护一个set,score存放的是时间戳,窗口元素都是24小时以内。

  1. 每次有新请求,先将时间戳位于窗口外的元素清除掉。
  2. set大小大于等于10,不放行,返回超过24小时限制。
  3. 判断set排名最大的元素的时间戳和当前时间戳是否超过60s,超过则放行,不超过返回60s限制。
  4. 判断set大小是否小于5,小于5则放行,并放入新元素。
  5. set大小小于10,大于等于5,取当前的set排名往前数5,即ZRANGE key size-5 size-5,拿出排行倒数第五的元素,判断是否超过一个小时,超过一个小时则可以放行,不超过返回1小时限制。

上述的执行应该以原子形式进行,防止出现不准确情况,这里采用lua脚本

lua脚本

local key = KEYS[1]
local limit1 = tonumber(ARGV[1])
local limit2 = tonumber(ARGV[2])
local windowStart = tonumber(ARGV[3])
local currentTime = tonumber(ARGV[4])

-- 清除窗口外的元素
redis.call('zremrangebyscore', key, 0 , windowStart)

-- 获取当前集合大小
local currentSize = tonumber(redis.call('zcard', key))

if currentSize >= limit2 then
    -- 集合大小大于等于 limit2,不放行,返回超过24小时限制
    return 0
end

-- 判断集合中最大元素与当前时间间隔是否超过60秒
local oldestTimestamp = tonumber(redis.call('zrange', key, -1, -1, 'WITHSCORES')[2])
if (currentTime - oldestTimestamp) < 60000 then
    -- 未超过60秒限制,返回60秒限制
    return 0
end

if currentSize < limit1 then
    -- 集合大小小于 limit1,放行请求并添加新元素
    redis.call('zadd', key, currentTime, currentTime)
    return 1
else
    -- 集合大小小于 limit2 且大于等于 limit1,判断是否超过1小时限制
    local hourAgoTimestamp = currentTime - 3600000          
    local fifthTimestamp = tonumber(redis.call('zrange', key, currentSize - limit1, currentSize - limit1, 'WITHSCORES')[2])
    if fifthTimestamp < hourAgoTimestamp then
        -- 未超过1小时限制,放行请求并添加新元素
        redis.call('zadd', key, currentTime, currentTime)
        return 1
    else
        -- 已超过1小时限制,返回1小时限制
        return 0
    end
end

java代码

import org.redisson.Redisson;
import org.redisson.api.RScript;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedisLuaScriptExample {

    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        // Lua 脚本
        String luaScript =
            "local key = KEYS[1] " +
            "local limit1 = tonumber(ARGV[1]) " +
            "local limit2 = tonumber(ARGV[2]) " +
            "local windowStart = tonumber(ARGV[3]) " +
            "local currentTime = tonumber(ARGV[4]) " +
            "redis.call('zremrangebyscore', key, '-inf', windowStart) " +
            "local currentSize = tonumber(redis.call('zcard', key)) " +
            "if currentSize >= limit2 then " +
            "  return 0 " +
            "end " +
            "local oldestTimestamp = tonumber(redis.call('zrange', key, -1, -1, 'WITHSCORES')[2]) " +
            "if (currentTime - oldestTimestamp) < 60000 then " +
            "  return 0 " +
            "end " +
            "if currentSize < limit1 then " +
            "  redis.call('zadd', key, currentTime, currentTime) " +
            "  return 1 " +
            "else " +
            "  local hourAgoTimestamp = currentTime - 3600000 " +
            "  local fifthTimestamp = tonumber(redis.call('zrange', key, currentSize - limit1 , currentSize - limit1, 'WITHSCORES')[2]) " +
            "  if fifthTimestamp < hourAgoTimestamp then " +
            "    redis.call('zadd', key, currentTime, currentTime) " +
            "    return 1 " +
            "  else " +
            "    return 0 " +
            "  end " +
            "end";

        RScript script = redisson.getScript();
        // 执行 Lua 脚本
        Long result = script.eval(RScript.Mode.READ_WRITE, luaScript, RScript.ReturnType.INTEGER,
                                  "your_key", // 这里替换成你的键
                                  "5",        // 替换成 limit1 的值
                                  "10",       // 替换成 limit2 的值
                                  String.valueOf(System.currentTimeMillis() - 86400000), // 24小时前的时间戳
                                  String.valueOf(System.currentTimeMillis()));
        System.out.println("Result: " + result);

        redisson.shutdown();
    }
}

与通过滑动窗口实现接口调用的多种限制策略相似的内容:

通过滑动窗口实现接口调用的多种限制策略

前言 有个邮箱发送的限制发送次数需求,为了防止用户恶意请求发送邮件的接口,对用户的发送邮件次数进行限制,每个邮箱60s内只能接收一次邮件,每个小时只能接收五次邮件,24小时只能接收十次邮件,一共有三个条件的限制。 实现方案 单机方案 单机简单实现可以用Caffeine,在Caffeine里面Key为

算法~利用zset实现滑动窗口限流

滑动窗口限流 滑动窗口限流是一种常用的限流算法,通过维护一个固定大小的窗口,在单位时间内允许通过的请求次数不超过设定的阈值。具体来说,滑动窗口限流算法通常包括以下几个步骤: 初始化:设置窗口大小、请求次数阈值和时间间隔。 维护窗口:将请求按照时间顺序放入窗口中,并保持窗口内请求数量不超过阈值。 检查

.NET 高效灵活的API速率限制解决方案

前言 FireflySoft.RateLimit是基于.NET Core和.NET Standard构建,支持多种速率限制算法和策略,包括固定窗口、滑动窗口、漏桶、令牌桶等。通过简单的配置和集成,开发者可以快速地将其应用到现有的Web API、微服务或中间件中,实现对请求的精确控制。 同时,该库还支

jdk17下netty导致堆内存疯涨原因排查

# 背景: ### 介绍 天网风控**灵玑**系统是基于内存计算实现的高吞吐低延迟在线计算服务,提供滑动或滚动窗口内的count、distinctCout、max、min、avg、sum、std及区间分布类的在线统计计算服务。客户端和服务端底层通过netty直接进行tcp通信,且服务端也是基于net

窗口到底有多滑动?揭秘TCP/IP滑动窗口的工作原理

本文将深入揭示TCP/IP滑动窗口的工作原理,探讨其在确保数据准确性和实现高效通信方面的重要性。

[转帖]TCP流量控制_(滑动窗口)

一、TCP vs. UDP TCP可提供可靠的数据传输而UDP无法做到,那我们为什么还用UDP? ·使用UDP传送单条消息的开销要比TCP小 ·响应式通信,UDP的速度要比TCP快。 DNS是应用UDP的绝好例子。 但使用UDP又需要可靠性保证的应用程序必须自行实现可靠性保障功能。 如果需要更高级的

C语言中的窗口滑动技术

学习文章:C语言中的窗口滑动技术 滑动窗口法 C语言中的窗口滑动技术 循环几乎是每个复杂问题的一部分。太多的循环/嵌套循环会增加所需的时间,从而增加程序的时间复杂性。窗口滑动技术是一种计算技术,用于减少程序中使用的嵌套循环的数量,通过用单个循环代替嵌套循环来提高程序的效率。 如果你熟悉计算机网络中的

限流器设计思路(浅入门)

目录令牌桶算法(Token Bucket)漏桶算法(Leaky Bucket)滑动窗口(Sliding Window)总结 限流器(Rate Limiter)是一种用于控制系统资源利用率和质量的重要机制。它通过限制单位时间内可以执行的操作数量,从而防止系统过载和保护服务的可靠性。在Java中,可以使

Blazor如何实现类似于微信的Tab切换?

是否有小伙伴在使用tab的时候想进行滑动切换Tab? 并且有滑动左出左进,右出右进的效果 ,本文将讲解怎么在Blazor中去通过滑动切换Tab 本文中的UI组件使用的是MASA Blazor,您也可以是其他的UI框架,这个并不影响实际的运行效果,本文案例是兼容PC和Android的,演示效果是and

鸿蒙HarmonyOS实战-ArkUI事件(触屏事件)

前言 触屏事件是指通过触摸屏幕来进行操作和交互的事件。常见的触屏事件包括点击(tap)、双击(double tap)、长按(long press)、滑动(swipe)、拖动(drag)等。触屏事件通常用于移动设备和平板电脑等具有触摸屏幕的设备上,用户可以通过触摸屏幕上的不同区域或者以不同的方式进