基于令牌桶算法对高并发接口的优化

news2024/12/23 23:11:06

业务背景

项目中有一个抽奖接口,此接口需要处理高并发问题以及使用脚本作弊的问题。

本文主要探讨如何最大程度地减少脚本作弊行为对抽奖业务的影响。

设计思路

如何减少脚本作弊行为对抽奖业务的影响

使用令牌桶算法,对频率过高的用户请求进行拦截

通过拦截部分流量,剩余的请求仍会影响公平性

对于连续达到令牌耗尽的次数超过限制的用户会视为异常用户,并暂时封禁其抽奖资格

如何设置令牌桶的参数

和前端人员协调好落下红包雨的速度,将令牌桶限流的阈值调的比前端红包雨落下的速度稍大即可

令牌桶算法

令牌桶限流是一种常用的流量控制算法,用于限制系统或服务对请求的处理速率。其原理基于令牌桶的概念,通过控制令牌的生成和消耗来实现流量的平滑控制。

在令牌桶限流算法中,令牌桶可以看作是一个存放令牌的容器,以固定的速率产生令牌。每个令牌代表系统可处理的一个请求。当请求到达时,首先需要获取一个令牌才能被处理。

令牌桶限流的实现逻辑如下:

  1. 令牌产生:令牌桶以恒定的速率生成令牌,例如每秒生成n个令牌。这个速率决定了系统允许的最大处理能力。

  2. 令牌消耗:每当请求到达时,需要尝试获取一个令牌。如果令牌桶中有可用的令牌,则请求被允许处理,并从令牌桶中消耗一个令牌。如果令牌桶中没有可用的令牌,则请求被暂时阻塞或丢弃。

此处参考本连接的算法并根据自己的业务需求进行改进:基于 Redis 和 Lua 实现分布式令牌桶限流 - 掘金 (juejin.cn)icon-default.png?t=N7T8https://juejin.cn/post/6922809716804419591

--[[
  1. key - 令牌桶的 key
  2. intervalPerTokens - 生成令牌的间隔(ms)
  3. curTime - 当前时间
  4. initTokens - 令牌桶初始化的令牌数
  5. bucketMaxTokens - 令牌桶的上限
  6. resetBucketInterval - 重置桶内令牌的时间间隔
  7. currentTokens - 当前桶内令牌数
  8. bucket - 当前 key 的令牌桶对象
]] --

local key = KEYS[1]
local intervalPerTokens = tonumber(ARGV[1])
local curTime = tonumber(ARGV[2])
local initTokens = tonumber(ARGV[3])
local bucketMaxTokens = tonumber(ARGV[4])
local resetBucketInterval = tonumber(ARGV[5])
-- 最大失败次数
local MAX_FAIL_TIMES = 20
-- 封禁时长
local BAN_DURATION = 60000

local bucket = redis.call('hgetall', key)
local currentTokens

-- 限流 判断是否作弊
local lockKey = "lock:" .. key
local newValue = redis.call('INCR', lockKey)
redis.call('EXPIRE', "lock:" .. key, 5000)
if newValue > MAX_FAIL_TIMES or newValue < -1 then
-- 用户行为异常 进行封禁
    redis.call('set', lockKey, -100000)
    redis.call('EXPIRE', lockKey, BAN_DURATION)
    return -1
end


-- 若当前桶未初始化,先初始化令牌桶
if table.maxn(bucket) == 0 then

    -- 初始桶内令牌
    currentTokens = initTokens
    
    -- 设置桶最近的填充时间是当前
    redis.call('hset', key, 'lastRefillTime', curTime)
    
    -- 初始化令牌桶的过期时间, 设置为间隔的 1.5 倍
    redis.call('pexpire', key, resetBucketInterval * 1.5)


-- 若桶已初始化,开始计算桶内令牌
-- 为什么等于 4 ? 因为有两对 field, 加起来长度是 4
-- { "lastRefillTime(上一次更新时间)","curTime(更新时间值)","tokensRemaining(当前保留的令牌)","令牌数" }
elseif table.maxn(bucket) == 4 then

    -- 上次填充时间
    local lastRefillTime = tonumber(bucket[2])
    -- 剩余的令牌数
    local tokensRemaining = tonumber(bucket[4])

    -- 当前时间大于上次填充时间
    if curTime > lastRefillTime then

        -- 拿到当前时间与上次填充时间的时间间隔
        -- 举例理解: curTime = 2620 , lastRefillTime = 2000, intervalSinceLast = 620
        local intervalSinceLast = curTime - lastRefillTime

        -- 如果当前时间间隔 大于 令牌的生成间隔
        -- 举例理解: intervalSinceLast = 620, resetBucketInterval = 1000
        if intervalSinceLast > resetBucketInterval then

            -- 将当前令牌填充满
            currentTokens = initTokens

            -- 更新重新填充时间
            redis.call('hset', key, 'lastRefillTime', curTime)

        -- 如果当前时间间隔 小于 令牌的生成间隔
        else

            -- 可授予的令牌 = 向下取整数( 上次填充时间与当前时间的时间间隔 / 两个令牌许可之间的时间间隔 )
            -- 举例理解 : intervalPerTokens = 200 ms , 令牌间隔时间为 200ms
            --           intervalSinceLast = 620 ms , 当前距离上一个填充时间差为 620ms
            --           grantedTokens = 620/200 = 3.1 = 3
            local grantedTokens = math.floor(intervalSinceLast / intervalPerTokens)

            -- 可授予的令牌 > 0 时
            -- 举例理解 : grantedTokens = 620/200 = 3.1 = 3
            if grantedTokens > 0 then

                -- 生成的令牌 = 上次填充时间与当前时间的时间间隔 % 两个令牌许可之间的时间间隔
                -- 举例理解 : padMillis = 620%200 = 20
                --           curTime = 2620
                --           curTime - padMillis = 2600
                local padMillis = math.fmod(intervalSinceLast, intervalPerTokens)

                -- 将当前令牌桶更新到上一次生成时间
                redis.call('hset', key, 'lastRefillTime', curTime - padMillis)
            end

            -- 更新当前令牌桶中的令牌数
            -- Math.min(根据时间生成的令牌数 + 剩下的令牌数, 桶的限制) => 超出桶最大令牌的就丢弃
            currentTokens = math.min(grantedTokens + tokensRemaining, bucketMaxTokens)
        end
    else
        -- 如果当前时间小于或等于上次更新的时间, 说明刚刚初始化, 当前令牌数量等于桶内令牌数
        -- 不需要重新填充
        currentTokens = tokensRemaining
    end
end

-- 如果当前桶内令牌小于 0,抛出异常
assert(currentTokens >= 0)

-- 如果当前令牌 == 0 ,更新桶内令牌, 返回 0
if currentTokens == 0 then
    redis.call('hset', key, 'tokensRemaining', currentTokens)

    return 0
else
    -- 如果当前令牌 大于 0, 更新当前桶内的令牌 -1 , 再返回当前桶内令牌数
    redis.call('hset', key, 'tokensRemaining', currentTokens - 1)
    return currentTokens
end

算法的实现逻辑如下:

  1. 首先,判断是否有作弊行为。如果某用户的请求失败次数超过预设的最大失败次数(MAX_FAIL_TIMES),或者失败次数小于-1(异常情况),则封禁该用户一段时间(BAN_DURATION)。
  2. 如果令牌桶尚未初始化,则进行初始化。将桶内的令牌数量设置为初始令牌数(initTokens),记录当前时间为最近一次填充时间(lastRefillTime),并设置令牌桶的过期时间为重置桶内令牌时间间隔的1.5倍。
  3. 如果令牌桶已初始化,则计算当前桶内的令牌数量。
    • 如果当前时间大于最近一次填充时间,说明需要进行令牌填充。
      • 如果当前时间与最近一次填充时间的时间间隔大于重置桶内令牌时间间隔,则将令牌桶中的令牌数量设置为初始令牌数,并更新最近一次填充时间为当前时间。
      • 如果当前时间间隔小于重置桶内令牌时间间隔,则根据时间间隔计算可授予的令牌数,并更新最近一次填充时间。同时,更新令牌桶中的令牌数量为可授予的令牌数和剩余令牌数的较小值。
    • 如果当前时间小于等于最近一次更新的时间,说明刚刚初始化,当前令牌数量为桶内令牌数,无需重新填充。
  4. 确保当前桶内的令牌数量大于等于0。
  5. 如果当前令牌数量为0,更新令牌桶中的令牌数量为0,并返回0。
  6. 如果当前令牌数量大于0,更新令牌桶中的令牌数量为当前令牌数量减1,并返回当前令牌数量。

部分业务代码

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method signatureMethod = signature.getMethod();
        Limit limit = signatureMethod.getAnnotation(Limit.class);

        String key = getCombinKey(limit, signatureMethod);
        List<String> keys = Collections.singletonList(key);

        String luaScript = buildLuaScript();
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        // 这个是调用lua脚本的代码
        Long count = rateLimiter.rateLimit(key, 5000, new Date().getTime(), 3, 100, 10000);
        if(count != null && count != 0 && count != -1){
            return point.proceed();
        }else if(count == -1){
            throw new BusinessException("账号有异常行为!");
        }
        else{
            throw new BusinessException("访问过于频繁!");
        }
    }

效果图

此处使用jmeter压测

此处附上github仓库的地址,如果觉得有用,请点一个珍贵的star,谢谢!

chenyi0008/lottery (github.com)icon-default.png?t=N7T8https://github.com/chenyi0008/lottery/tree/chen

具体实现的代码在此处

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1581567.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

关于ansible的模块 ⑤

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 继《关于Ansible的模块 ①》、《关于Ansible的模块 ②》、《关于Ansible的模块 ③》与《关于Ansible的模块 ④》之后&#xff0c…

uniapp 地图分幅网格生成 小程序基于map组件

// 获取小数部分 const fractional function(x) {x Math.abs(x);return x - Math.floor(x); } const formatInt function(x, len) {let result x;len len - result.length;while (len > 0) {result 0 result;len--;}return result; }/*** 创建标准分幅网格* param …

Mysql-数据库集群的搭建以及数据库的维护

一、数据库的维护 1.数据库的备份与恢复 1&#xff09;备份指定数据库 #mysqldump -u root -p zx > ./zx.dump 2&#xff09;备份所有库 #mysqldump -u root -p --all-databases > ./all.dump 3)恢复所有库 #mysql -u root -p < ./all.dump 4)恢复指定数据库 #mysq…

SqlServer快速导出数据库结构的方法

1、查询出所有的表 SELECT name, id From sysobjects WHERE xtype u ORDER BY name ASC 2、根据表名查询出表结构 select syscolumns.name as "列名", systypes.name as "数据类型", syscolumns.length as "数据长度", sys.extended_prope…

【数据结构】考研真题攻克与重点知识点剖析 - 第 7 篇:查找

前言 本文基础知识部分来自于b站&#xff1a;分享笔记的好人儿的思维导图与王道考研课程&#xff0c;感谢大佬的开源精神&#xff0c;习题来自老师划的重点以及考研真题。此前我尝试了完全使用Python或是结合大语言模型对考研真题进行数据清洗与可视化分析&#xff0c;本人技术…

Redis中的集群(三)

集群 槽指派 记录节点的槽指派信息。 clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽: struct clusterNode { // ... unsigned char slots[16384/8];int numslots; // ... }slots属性是一个二进制位数组(bit array)&#xff0c;这个数组的长度位16384/8…

CLI的使用与IOS基本命令

1、实验目的 通过本实验可以掌握&#xff1a; CLI的各种工作模式个CLI各种编辑命令“?” 和【Tab】键使用方法IOS基本命令网络设备访问限制查看设备的相关信息 2、实验拓扑 CLI的使用与IOS基本命令使用拓扑如下图所示。 3、实验步骤 &#xff08;1&#xff09;CLI模式的切…

《前端面试题》- CSS - CSS选择器的优先级

行内样式1000 d选择器100 属性选择器、class或者伪类10 元素选择器&#xff0c;或者伪元素1 通配符0 参考网址&#xff1a;https://blog.csdn.net/jbj6568839z/article/details/113888600https://www.cnblogs.com/RenshuozZ/p/10327285.htmlhttps://www.cnblogs.com/zxjwlh/p/6…

【机器学习】一文掌握机器学习十大分类算法(上)。

十大分类算法 1、引言2、分类算法总结2.1 逻辑回归2.1.1 核心原理2.1.2 算法公式2.1.3 代码实例 2.2 决策树2.2.1 核心原理2.2. 代码实例 2.3 随机森林2.3.1 核心原理2.3.2 代码实例 2.4 支持向量机2.4.1 核心原理2.4.2 算法公式2.4.3 代码实例 2.5 朴素贝叶斯2.5.1 核心原理2.…

mybatis-plus与mybatis同时使用别名问题

在整合mybatis和mybatis-plus的时候发现一个小坑&#xff0c;单独使用mybatis&#xff0c;配置别名如下&#xff1a; #配置映射文件中指定的实体类的别名 mybatis.type-aliases-packagecom.jk.entity XML映射文件如下&#xff1a; <update id"update" paramete…

骑砍2霸主MOD开发(2)-基础开发环境搭建

一.骑砍2霸主程序架构 二.骑砍2霸主C#接口层代码查看 1.C#反编译工具dnspy下载: 2.骑砍2霸主游戏引擎接口查看: 例如IMBAgent interface接口: #调用TaleWorlds.Native.dll中的函数 [EngineMethod("get_movement_flags", false)] uint GetMovementFlags(UIntPtr agen…

MySQL高级(索引语法、创建索引、查看索引、删除索引)

创建索引 create [unique | fulltext] index index_name on table_name (index_col_name,...); 查看索引 show index from table_name; 删除索引 drop index index_name on table_name; 案例演示&#xff1a; 先来创建一张表 tb_user&#xff0c;并且查询测试数据。 cre…

OLAP在线实时 数据分析平台

随着业务的增长&#xff0c;精细化运营的提出&#xff0c;产品对数据部门提出了更高的要求&#xff0c;包括需要对实时数据进行查询分析&#xff0c;快速调整运营策略&#xff1b;对小部分人群做 AB 实验&#xff0c;验证新功能的有效性&#xff1b;减少数据查询时间&#xff0…

easyExcel - 动态复杂表头的编写

目录 前言一、情景介绍二、问题分析三、代码实现方式一&#xff1a;head 设置方式二&#xff1a;模板导出方式三&#xff1a;自定义工具类 前言 Java-easyExcel入门教程&#xff1a;https://blog.csdn.net/xhmico/article/details/134714025 之前有介绍过如何使用 easyExcel&…

Java基于微信小程序的日语学习小程序,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

IMU状态预积分噪声模型

IMU状态预积分噪声模型 IMU状态预积分噪声模型旋转部分速度部分平移部分总结 IMU状态预积分噪声模型 根据之前的推导&#xff0c;得出了IMU状态预积分的测量模型&#xff0c;同时得到了噪声部分的定义公式&#xff0c;其中噪声部分罗列如下&#xff1a; 由于噪声项的定义比较复…

51单片机+TN901非接触式红外测温设计论文与源码PCB等资料

1、摘要 温度测量技术应用十分广泛&#xff0c;而且在现代设备故障检测领域中也是一项非常重要的技术。但在某些应用领域中&#xff0c;要求测量温度用的传感器不能与被测物体相接触&#xff0c;这就需要一种非接触的测温方式来满足上述测温需求。本论文正是应上述实际需求而设…

【2024】Rancher的安装与介绍

———————————————————————————— 记录一下rancher的学习与使用过程 本部分内容包括rancher的介绍、特点、与k8s关系和部署等内容 ———————————————————————————— Rancher是什么&#xff1f; 简单来说&#xff0c;Ranc…

RabbitMQ如何保证消息的幂等性???

在RabbitMQ中&#xff0c;保证消费者的幂等性主要依赖于业务设计和实现&#xff0c;而非RabbitMQ本身提供的一种直接功能。 在基于Spring Boot整合RabbitMQ的场景下&#xff0c;要保证消费者的幂等性&#xff0c;通常需要结合业务逻辑设计以及额外的技术手段来实现。以下是一个…

由近期 RAGFlow 的火爆看 RAG 的现状与未来

4 月 1 日&#xff0c;InfiniFlow &#xff08;英飞流&#xff09;的端到端 RAG 解决方案 RAGFlow 正式开源&#xff0c;首日即获得了 github 千星&#xff0c;目前已接近 3000 star。在这之前&#xff0c;InfiniFlow 还开源了专门用于 RAG 场景的 AI 原生数据库 Infinity&…