redis+lua 实现接口限流

虽然Redis 4.0 提供了一个限流 Redis 模块,它叫 redis-cell。该模块也使用了漏斗算法,并提供了原子的限流指令。有了这个模块,限流问题就非常简单了。

但是我们用4.0以下的版本怎么办?还好经过我的查找我还是找到了解决的办法的。

经过我的查找有两个方案:
方案一:计数器法,实现比较简单,但是可能在一个窗口期内会有两倍的请求量。所以舍弃。
方案二:用zset数据结构,实现窗口滑动,但是这个方案如果是几千用户还行,如果是几十万上百万用户的话,那这个内存消耗太巨大了。
方案三:Redis+lua 实现令牌桶。这时我目前找到最合适的方案了
我们重点讲一下方案三:为啥要用lua,因为redis读取lua脚本可以实现原子性操作,所以原子性的问题就解决了,而且令牌桶的这个方案就是漏斗算法来的,很好的解决了窗口期两倍请求量的问题。


public class VisitLimitUtils {

    private static VisitLimitUtils visitLimitUtils = new VisitLimitUtils();

    public static VisitLimitUtils getInstance(){
        return visitLimitUtils;
    }

    /**
     *
     * @param jedis
     * @param lockKey 添加令牌Key
     * @return
     */
    public  boolean set(Jedis jedis, String lockKey) {
        lockKey = "visitLimit:"+lockKey;
        String script = getLuaScript();
        List<String> argvList =  new ArrayList<>();
        //桶最大容量
        argvList.add(GameContext.redisBucketapacity);
        //每次添加令牌数
        argvList.add(GameContext.redisAddToken);
        //令牌添加间隔(毫秒)
        argvList.add(GameContext.redisAddInterval);
        argvList.add(System.currentTimeMillis()+"");
        Long result = (Long) jedis.eval(script, Collections.singletonList(lockKey), argvList);
        Long RELEASE_SUCCESS = -1L;
        //使用完的jedis连接要释放会连接池
        jedis.close();

        if (RELEASE_SUCCESS.equals(result)) {
            return false;
        }
        return true;
    }

    /**
     * 获取lua脚本,令牌桶限流,redis执行lua脚本可以保证原子性
     * @return
     */
    private String getLuaScript(){
        /*         KEYS[1] string 限流的key
         ARGV[1] int  桶最大容量
         ARGV[2] int  每次添加令牌数
         ARGV[3] int  令牌添加间隔(毫秒)
         ARGV[4] int  当前时间戳*/
        return  "local bucket_capacity = tonumber(ARGV[1])\n" +
                "local add_token = tonumber(ARGV[2])\n" +
                "local add_interval = tonumber(ARGV[3])\n" +
                "local now = tonumber(ARGV[4])\n" +
                " \n" +
                "-- 保存上一次更新桶的时间的key\n" +
                "local LAST_TIME_KEY = KEYS[1]..\"_time\";   \n" +
                "-- 获取当前桶中令牌数\n" +
                "local token_cnt = redis.call(\"get\", KEYS[1]) \n" +
                "-- 桶完全恢复需要的最大时长\n" +
                "local reset_time = math.ceil(bucket_capacity / add_token) * add_interval;\n" +
                " \n" +
                "if token_cnt then -- 令牌桶存在\n" +
                " -- 上一次更新桶的时间\n" +
                " local last_time = redis.call('get', LAST_TIME_KEY)\n" +
                " -- 恢复倍数\n" +
                " local multiple = math.floor((now - last_time) / add_interval)\n" +
                " -- 恢复令牌数\n" +
                " local recovery_cnt = multiple * add_token\n" +
                " -- 确保不超过桶容量\n" +
                " local token_cnt = math.min(bucket_capacity, token_cnt + recovery_cnt) - 1\n" +
                "  \n" +
                " if token_cnt < 0 then\n" +
                "  return -1;\n" +
                " end\n" +
                "  \n" +
                " -- 重新设置过期时间, 避免key过期\n" +
                " redis.call('set', KEYS[1], token_cnt, 'EX', reset_time)      \n" +
                " redis.call('set', LAST_TIME_KEY, last_time + multiple * add_interval, 'EX', reset_time)\n" +
                " return token_cnt\n" +
                "  \n" +
                "else -- 令牌桶不存在\n" +
                " token_cnt = bucket_capacity - 1\n" +
                " -- 设置过期时间避免key一直存在\n" +
                " redis.call('set', KEYS[1], token_cnt, 'EX', reset_time);\n" +
                " redis.call('set', LAST_TIME_KEY, now, 'EX', reset_time + 1000); \n" +
                " return token_cnt \n" +
                "end";
    }

    public static void main(String[] args) throws InterruptedException {
        HttpDispatcher.getInstance().load("com.cloud.gold");
        Jedis jedis = RedisUtil.INSTANCE.getJedis();
        for (;;){
            System.out.println(VisitLimitUtils.getInstance().set(jedis,"10001"));
        }
    }

}

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis 中创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本来实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本来实现漏桶算法。 具体实现步骤如下: - 在 Redis 中创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值