Redis 实现多规则限流的思考与实践( 二 )


  • 编写生成 RedisKey 的方法
 /** * 通过 rateLimiter 和 joinPoint 拼接prefix : ip / userId : classSimpleName - methodName * * @param rateLimiter 提供 prefix * @param joinPoint提供 classSimpleName : methodName * @return */public String getCombineKey(RateLimiter rateLimiter, JoinPoint joinPoint) {StringBuffer key = new StringBuffer(rateLimiter.key());// 不同限流类型使用不同的前缀switch (rateLimiter.limitType()) {// XXX 可以新增通过参数指定参数进行限流case IP:key.Append(IpUtil.getIpAddr(((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest())).append(":");break;case USER_ID:SysUserDetails user = SecurityUtil.getUser();if (!ObjectUtils.isEmpty(user)) key.append(user.getUserId()).append(":");break;case GLOBAL:break;}MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();Class<?> targetClass = method.getDeclaringClass();key.append(targetClass.getSimpleName()).append("-").append(method.getName());return key.toString();}编写lua脚本编写lua脚本 (两种将时间添加到Redis的方法) 。
Zset的UUID value值UUID(可用其他有相同的特性的值)为Zset中的value值
  • 参数介绍
KEYS[1] = prefix : ? : className : methodName
KEYS[2] = 唯一ID
KEYS[3] = 当前时间
ARGV = [次数 , 单位时间,次数,单位时间, 次数, 单位时间 ...]
  • 由java传入分布式不重复的 value 值
-- 1. 获取参数local key = KEYS[1]local uuid = KEYS[2]local currentTime = tonumber(KEYS[3])-- 2. 以数组最大值为 ttl 最大值local expireTime = -1;-- 3. 遍历数组查看是否超过限流规则for i = 1, #ARGV, 2 dolocal rateRuleCount = tonumber(ARGV[i])local rateRuleTime = tonumber(ARGV[i + 1])-- 3.1 判断在单位时间内访问次数local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)-- 3.2 判断是否超过规定次数if tonumber(count) >= rateRuleCount thenreturn trueend-- 3.3 判断元素最大值,设置为最终过期时间if rateRuleTime > expireTime thenexpireTime = rateRuleTimeendend-- 4. redis 中添加当前时间redis.call('ZADD', key, currentTime, uuid)-- 5. 更新缓存过期时间redis.call('PEXPIRE', key, expireTime)-- 6. 删除最大时间限度之前的数据,防止数据过多redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)return false根据时间戳作为Zset中的value值
  • 参数介绍
KEYS[1] = prefix : ? : className : methodName
KEYS[2] = 当前时间
ARGV = [次数,单位时间,次数 , 单位时间, 次数, 单位时间 ...]
  • 根据时间进行生成value值 , 考虑同一毫秒添加相同时间值问题
  • 以下为第二种实现方式 , 在并发高的情况下效率低,value是通过时间戳进行添加,但是访问量大的话会使得一直在调用 redis.call('ZADD', key, currentTime, currentTime),但是在不冲突value的情况下 , 会比生成 UUID 好
-- 1. 获取参数local key = KEYS[1]local currentTime = KEYS[2]-- 2. 以数组最大值为 ttl 最大值local expireTime = -1;-- 3. 遍历数组查看是否越界for i = 1, #ARGV, 2 dolocal rateRuleCount = tonumber(ARGV[i])local rateRuleTime = tonumber(ARGV[i + 1])-- 3.1 判断在单位时间内访问次数local count = redis.call('ZCOUNT', key, currentTime - rateRuleTime, currentTime)-- 3.2 判断是否超过规定次数if tonumber(count) >= rateRuleCount thenreturn trueend-- 3.3 判断元素最大值,设置为最终过期时间if rateRuleTime > expireTime thenexpireTime = rateRuleTimeendend-- 4. 更新缓存过期时间redis.call('PEXPIRE', key, expireTime)-- 5. 删除最大时间限度之前的数据,防止数据过多redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - expireTime)-- 6. redis 中添加当前时间( 解决多个线程在同一毫秒添加相同 value 导致 Redis 漏记的问题 )-- 6.1 maxRetries 最大重试次数 retries 重试次数local maxRetries = 5local retries = 0while true dolocal result = redis.call('ZADD', key, currentTime, currentTime)if result == 1 then-- 6.2 添加成功则跳出循环breakelse-- 6.3 未添加成功则 value + 1 再次进行尝试retries = retries + 1if retries >= maxRetries then-- 6.4 超过最大尝试次数 采用添加随机数策略local random_value = https://www.isolves.com/it/sjk/Redis/2024-01-03/math.random(1, 1000)currentTime = currentTime + random_valueelsecurrentTime = currentTime + 1endendendreturn false


推荐阅读