赞
踩
项目中有一个抽奖接口,此接口需要处理高并发问题以及使用脚本作弊的问题。
本文主要探讨如何最大程度地减少脚本作弊行为对抽奖业务的影响。
使用令牌桶算法,对频率过高的用户请求进行拦截
对于连续达到令牌耗尽的次数超过限制的用户会视为异常用户,并暂时封禁其抽奖资格
和前端人员协调好落下红包雨的速度,将令牌桶限流的阈值调的比前端红包雨落下的速度稍大即可
令牌桶限流是一种常用的流量控制算法,用于限制系统或服务对请求的处理速率。其原理基于令牌桶的概念,通过控制令牌的生成和消耗来实现流量的平滑控制。
在令牌桶限流算法中,令牌桶可以看作是一个存放令牌的容器,以固定的速率产生令牌。每个令牌代表系统可处理的一个请求。当请求到达时,首先需要获取一个令牌才能被处理。
令牌桶限流的实现逻辑如下:
令牌产生:令牌桶以恒定的速率生成令牌,例如每秒生成n个令牌。这个速率决定了系统允许的最大处理能力。
令牌消耗:每当请求到达时,需要尝试获取一个令牌。如果令牌桶中有可用的令牌,则请求被允许处理,并从令牌桶中消耗一个令牌。如果令牌桶中没有可用的令牌,则请求被暂时阻塞或丢弃。
此处参考本连接的算法并根据自己的业务需求进行改进:基于 Redis 和 Lua 实现分布式令牌桶限流 - 掘金 (juejin.cn)https://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
算法的实现逻辑如下:
- @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)https://github.com/chenyi0008/lottery/tree/chen
具体实现的代码在此处
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。