公司做网站公司,惠州网站制作案例,龙岩做网站开发找哪家,标准网站建设哪家好业务背景
项目中有一个抽奖接口#xff0c;此接口需要处理高并发问题以及使用脚本作弊的问题。
本文主要探讨如何最大程度地减少脚本作弊行为对抽奖业务的影响。 设计思路
如何减少脚本作弊行为对抽奖业务的影响
使用令牌桶算法#xff0c;对频率过高的用户请求进行拦截 …业务背景
项目中有一个抽奖接口此接口需要处理高并发问题以及使用脚本作弊的问题。
本文主要探讨如何最大程度地减少脚本作弊行为对抽奖业务的影响。 设计思路
如何减少脚本作弊行为对抽奖业务的影响
使用令牌桶算法对频率过高的用户请求进行拦截
通过拦截部分流量剩余的请求仍会影响公平性
对于连续达到令牌耗尽的次数超过限制的用户会视为异常用户并暂时封禁其抽奖资格
如何设置令牌桶的参数
和前端人员协调好落下红包雨的速度将令牌桶限流的阈值调的比前端红包雨落下的速度稍大即可 令牌桶算法
令牌桶限流是一种常用的流量控制算法用于限制系统或服务对请求的处理速率。其原理基于令牌桶的概念通过控制令牌的生成和消耗来实现流量的平滑控制。
在令牌桶限流算法中令牌桶可以看作是一个存放令牌的容器以固定的速率产生令牌。每个令牌代表系统可处理的一个请求。当请求到达时首先需要获取一个令牌才能被处理。
令牌桶限流的实现逻辑如下 令牌产生令牌桶以恒定的速率生成令牌例如每秒生成n个令牌。这个速率决定了系统允许的最大处理能力。 令牌消耗每当请求到达时需要尝试获取一个令牌。如果令牌桶中有可用的令牌则请求被允许处理并从令牌桶中消耗一个令牌。如果令牌桶中没有可用的令牌则请求被暂时阻塞或丢弃。 此处参考本连接的算法并根据自己的业务需求进行改进基于 Redis 和 Lua 实现分布式令牌桶限流 - 掘金 (juejin.cn)https://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
算法的实现逻辑如下
首先判断是否有作弊行为。如果某用户的请求失败次数超过预设的最大失败次数MAX_FAIL_TIMES或者失败次数小于-1异常情况则封禁该用户一段时间BAN_DURATION。如果令牌桶尚未初始化则进行初始化。将桶内的令牌数量设置为初始令牌数initTokens记录当前时间为最近一次填充时间lastRefillTime并设置令牌桶的过期时间为重置桶内令牌时间间隔的1.5倍。如果令牌桶已初始化则计算当前桶内的令牌数量。 如果当前时间大于最近一次填充时间说明需要进行令牌填充。 如果当前时间与最近一次填充时间的时间间隔大于重置桶内令牌时间间隔则将令牌桶中的令牌数量设置为初始令牌数并更新最近一次填充时间为当前时间。如果当前时间间隔小于重置桶内令牌时间间隔则根据时间间隔计算可授予的令牌数并更新最近一次填充时间。同时更新令牌桶中的令牌数量为可授予的令牌数和剩余令牌数的较小值。如果当前时间小于等于最近一次更新的时间说明刚刚初始化当前令牌数量为桶内令牌数无需重新填充。确保当前桶内的令牌数量大于等于0。如果当前令牌数量为0更新令牌桶中的令牌数量为0并返回0。如果当前令牌数量大于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);ListString keys Collections.singletonList(key);String luaScript buildLuaScript();RedisScriptLong 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)https://github.com/chenyi0008/lottery/tree/chen
具体实现的代码在此处