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

业务背景

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

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

设计思路

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

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

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

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

如何设置令牌桶的参数

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

令牌桶算法

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

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

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

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

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

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

--[[1. key - 令牌桶的 key2. 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 = 60000local 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 = 620local intervalSinceLast = curTime - lastRefillTime-- 如果当前时间间隔 大于 令牌的生成间隔-- 举例理解: intervalSinceLast = 620, resetBucketInterval = 1000if 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 = 3local grantedTokens = math.floor(intervalSinceLast / intervalPerTokens)-- 可授予的令牌 > 0 时-- 举例理解 : grantedTokens = 620/200 = 3.1 = 3if grantedTokens > 0 then-- 生成的令牌 = 上次填充时间与当前时间的时间间隔 % 两个令牌许可之间的时间间隔-- 举例理解 : padMillis = 620%200 = 20--           curTime = 2620--           curTime - padMillis = 2600local padMillis = math.fmod(intervalSinceLast, intervalPerTokens)-- 将当前令牌桶更新到上一次生成时间redis.call('hset', key, 'lastRefillTime', curTime - padMillis)end-- 更新当前令牌桶中的令牌数-- Math.min(根据时间生成的令牌数 + 剩下的令牌数, 桶的限制) => 超出桶最大令牌的就丢弃currentTokens = math.min(grantedTokens + tokensRemaining, bucketMaxTokens)endelse-- 如果当前时间小于或等于上次更新的时间, 说明刚刚初始化, 当前令牌数量等于桶内令牌数-- 不需要重新填充currentTokens = tokensRemainingend
end-- 如果当前桶内令牌小于 0,抛出异常
assert(currentTokens >= 0)-- 如果当前令牌 == 0 ,更新桶内令牌, 返回 0
if currentTokens == 0 thenredis.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.mzph.cn/news/803791.shtml

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

相关文章

pdffactory pro 8注册码序列号下载 附教程

PdfFactory Pro可以说是一款行业专业且技术领先的的PDF虚拟打印机软件。其不仅占用系统内存小巧&#xff0c;功能强大&#xff0c;可支持用户无需使用Acrobat来创建Adobe PDF即可以进行PDF组件的创建和打印。同时&#xff0c;现在全新的PdfFactory Pro 8也正式上线来袭&#xf…

(源码+部署+讲解)基于Spring Boot + Vue编程学习平台的设计与实现

前言 &#x1f497;博主介绍&#xff1a;✌专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅&#x1f447;&#x1f3fb; 2024年Java精品实战案例《100套》 &#x1f345;文末获取源码联系&#x1f345; &#x1f31f;…

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

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

开源铱塔切换MySQL数据库启动报异常

1.错误日志&#xff1a; 铱塔切换数据库配置为MySQL之后&#xff0c;启动后报错如下&#xff1a; SqlExceptionHelper - Table iotkit.task_info doesnt exist SqlExceptionHelper - Table iotkit.rule_info doesnt exist SqlExceptionHelper - Table iotkit.device_info does…

(WSI分类)WSI分类文献小综述 2024

2024的WSI分类。 Multiple Instance Learning Framework with Masked Hard Instance Mining for Whole Slide Image Classification &#xff08;ICCV2024&#xff09; 由于阳性组织只占 Gi- gapixel WSI 的一小部分&#xff0c;因此现有的 MIL 方法直观上侧重于通过注意力机…

Redis的常见命令

单线程&#xff1a;每个命令具备原子性 低延迟&#xff0c;速度快&#xff08;基于内存、IO多路复用、良好的编码&#xff09; 支持数据持久化 支持主从集群、分片集群 支持多语言客户端 2.Redis数据库介绍 Redis是一个key-value的数据库&#xff0c;key一般是String类型…

(源码+部署+讲解)基于Spring Boot + Vue的车位租赁系统设计与实现

前言 &#x1f497;博主介绍&#xff1a;✌专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅&#x1f447;&#x1f3fb; 2024年Java精品实战案例《100套》 &#x1f345;文末获取源码联系&#x1f345; &#x1f31f;…

Apache Incubator Answer 本地开发部署

文章目录 简介Github文档插件部署 Answer开发环境编译项目初始化项目运行项目 简介 一款适合任何团队的问答平台软件。 Apache Incubator Answer是一个开源项目&#xff0c;它是一个用于构建和部署问答系统的框架。该项目是Apache软件基金会的孵化器项目&#xff0c;提供一个…

【centos】Redis离线安装配置教程

Linux 离线安装Redis配置教程 一、下载二、安装redis三、设置redis开机自启&#xff0c;并且添加到系统服务四、gcc安装 redis官网地址&#xff1a;https://redis.io/ 一、下载 【点击进入下载地址&#xff1a;http://download.redis.io/releases/】选择安装包&#xff1a;re…

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 …

STM32学习和实践笔记(6):自己进行时钟配置的思路

在《STM32学习和实践笔记&#xff08;4&#xff09;: 分析和理解GPIO_InitTypeDef GPIO_InitStructure (d)-CSDN博客》 中&#xff0c;我了解到&#xff0c;在程序执行我们写的main函数之前&#xff0c;实际上先执行了一个汇编语言所写的启动文件&#xff0c;以完成相应的初始…

django celery 异步任务 异步存储

环境&#xff1a;win11、python 3.9.2、django 4.2.11、celery 4.4.7、MySQL 8.1、redis 3.0 背景&#xff1a;基于django框架的大量任务实现&#xff0c;并且需要保存数据库 时间&#xff1a;20240409 说明&#xff1a;异步爬取小说&#xff0c;并将其保存到数据库 1、创建…

配置交换机SSH管理和端口安全——实验2:配置交换机端口安全

实验目的 通过本实验可以掌握&#xff1a; 交换机管理地址配置及接口配置。查看交换机的MAC地址表。配置静态端口安全、动态端口安全和粘滞端口安全的方法 实验拓扑 配置交换机端口安全的实验拓扑如图所示。 配置交换机端口安全的实验拓扑 实验步骤 &#xff08;1&#x…

springboot+vue2+elementui+mybatis- 批量导出导入

全部导出 批量导出 报错问题分析 经过排查&#xff0c;原因是因为在发起 axios 请求的时候&#xff0c;没有指定响应的数据类型&#xff08;这里需要指定响应的数据类型为 blob 二进制文件&#xff09; 当响应数据回来后&#xff0c;会执行 axios 后置拦截器的代码&#xff0…

[开源] 基于transformer的时间序列预测模型python代码

分享一下基于transformer的时间序列预测模型python代码&#xff0c;给大家&#xff0c;记得点赞哦 #!/usr/bin/env python # coding: 帅帅的笔者import torch import torch.nn as nn import numpy as np import pandas as pd import time import math import matplotlib.pyplo…

【Java8新特性】二、函数式接口

这里写自定义目录标题 一、什么是函数式接口二、自定义函数式接口三、作为参数传递 Lambda 表达式四、四大内置核心函数式接口1、消费形接口2、供给形接口3、函数型接口4、断言形接口 一、什么是函数式接口 只包含一个抽象方法的接口&#xff0c;称为函数式接口。你可以通过 L…

【MATLAB高级编程】第二篇 | 元胞数组(cell)操作

【第二篇】元胞数组&#xff08;cell&#xff09;操作 1. 创建元胞数组cell2. 查看和修改cell内的元素值3. 高级操作: 可视化作图显示cell内的内容4. 把矩阵转换成单元数组5. 把单元数组转换成结构体变量 你好&#xff01; 欢迎进入 《MATLAB高级编程》 文章系列 &#xff0c;每…

postgresql uuid

示例数据库版本PG16&#xff0c;对于参照官方文档截图&#xff0c;可以在最上方切换到对应版本查看&#xff0c;相差不大。 方法一&#xff1a;自带函数 select gen_random_uuid(); 去掉四个斜杠&#xff0c;简化成32位 select replace(gen_random_uuid()::text, -, ); 官网介绍…

《前端面试题》- 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…

搭建Grafana+Prometheus监控Spring Boot应用

Spring项目改造 maven依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId> </dependency><dependency><groupId>io.micrometer</groupId><artif…