基于 Redis 实现分布式限流

这篇具有很好参考价值的文章主要介绍了基于 Redis 实现分布式限流。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、 简介

分布式限流是指通过将限流策略嵌入到分布式系统中,以控制流量或保护服务,保证系统在高并发访问情况下不被过载。

分布式限流可以防止系统因大量请求同时到达导致压力过大而崩溃,从而提高系统的稳定性和可靠性。同时,它可以使得业务资源能够更好地分配,提高系统的效率。

二、分布式限流

1 数据结构

1.1 Redis List

Redis List 是一个可以输入相同元素和非唯一元素的集合且支持在两端进行快速(O(1))插入和删除元素。

1.2 Redis Set

Redis Set 是一个无序,但不允许重复元素的集合。您可以使用这些命令对Redis集进行常规操作: SADD,SREM,SISMEMBER,SMEMBERS等。

1.3 Redis Sorted Set

Redis Sorted Set 是一个有序的、不重复的元素集合。每个元素都关联着一个浮点数值(称为 Score)。通过 Score 可以从小到大排序得到一个有序集合。

2 实现分布式限流

在Redis中可以使用令牌桶算法来实现分布式限速。具体方法为:

Step 1:创建一个列表作为Redis令牌桶

String key = "rate_limit:" + userId;
// 模拟用户请求访问
List<String> tokens = redis.lrange(key, 0, -1);

Step 2:设定令牌桶的基准参数

int maxTokens = 50;
long timeInterval = 1 * 1000;
long now = System.currentTimeMillis();

Step 3:计算Redis中令牌数量

long expiredTokens = tokens.stream().filter(t -> Long.parseLong(t) < now - timeInterval).count();
tokens = tokens.subList((int) expiredTokens, tokens.size());
long remainTokens = maxTokens - tokens.size();

Step 4:基于令牌数量,判断是否超过限制

if (remainTokens < 1) {
    throw new RateLimitException("请求太频繁,请稍后再试!");
}

Step 5:如果没有超出限制,则更新Redis中令牌数并设置过期时间

Long expiresIn = now + timeInterval;
redis.multi();
redis.rpush(key, String.valueOf(expiresIn));
redis.pexpire(key, timeInterval);
redis.exec();

3 实现原理分析

以上代码所示首先需要建立Redis List用于存储Token,其次需要设定令牌桶的基准参数(比如最大Token数量和Token过期间隔等)。在用户访问请求时,需要计算Redis中的令牌数量,根据规则对访问量进行限制。如果没有超过限制,则需要更新Redis List中令牌数并设置过期时间;如果超过了限制,则需要返回错误信息并拒绝服务。

整个过程中,需要注意并发访问情况下的线程安全问题,并确保流量控制配置的公共协商,如最大QPS(Queries Per Second),哪些接口需限制流量等。

三、分布式限流算法

在实际的系统设计中,为了防止某一时刻出现大量请求导致系统崩溃,我们通常会采用限流策略来控制流量,而Redis作为分布式NoSQL数据库,在限流中也有着广泛的应用。下面介绍一些Redis分布式限流的经典算法。

1. 计数器算法

计数器算法比较简单,直接利用Redis存储每个IP或者用户的请求次数,当请求次数超过预设阈值时拒绝服务。代码如下:

public boolean isAllowed(String key, int limit, int timeout) {
    Jedis jedis = getJedis();
    long count = jedis.incr(key);
    if (count == 1) {
        jedis.expire(key, timeout);
    }
    boolean allowed = count <= limit;
    if (!allowed) {
        jedis.del(key);
    }
    jedis.close();
    return allowed;
}
  • key:需要限流的用户标识,可根据IP、UserID等进行定义
  • limit:阈值,即允许的最大请求数
  • timeout:过期时间,对于计数器算法,一定要设置过期时间,否则缓存中的请求次数会一直不断累加

2. 漏斗算法

漏斗算法的核心思想是将请求按照恒定的速率转换为水流,有效控制请求超出服务处理能力的情况。漏斗算法实现代码如下:

public boolean isAllowed(String key, int capacity, double leakRate, int reqCount) {
    Jedis jedis = getJedis();
    long nowTime = System.currentTimeMillis();
    String luaScript =
          "local currentCapacity = tonumber(redis.call('hget', KEYS[1], 'leftCapacity'))\n"
        + "if currentCapacity == nil then\n"
        + "    redis.call('hset', KEYS[1], 'lastTime', ARGV[2])\n"
        + "    redis.call('hset', KEYS[1], 'leftCapacity', ARGV[1] - 1)\n"
        + "    return 1\n"
        + "end\n"
        + "local changeTime = tonumber(redis.call('hget', KEYS[1], 'lastTime'))\n"
        + "local delayMillSeconds = nowTime - changeTime\n"
        + "local currentDelayCount = tonumber(delayMillSeconds*ARGV[3])\n"
        + "local currentCapacity = math.min(currentDelayCount+currentCapacity, ARGV[1])\n"
        + "if currentCapacity >= ARGV[4] then\n"
        + "    return 0\n"
        + "else\n"
        + "    redis.call('hset', KEYS[1], 'leftCapacity', currentCapacity-1)\n"
        + "    redis.call('hset', KEYS[1], 'lastTime', nowTime)\n"
        + "    return 1\n"
        + "end";
    Object result = jedis.eval(
        luaScript,
        Collections.singletonList(key),
        Arrays.asList(String.valueOf(capacity), String.valueOf(nowTime), String.valueOf(leakRate), String.valueOf(reqCount))
    );
    boolean allowed = (result instanceof Long ? (Long) result : 0L) == 1L;
    jedis.close();
    return allowed;
}
  • key:需要进行限流的用户标识
  • capacity:漏斗容量,即最大允许请求数量
  • leakRate:漏嘴流水速率,保证有序的请求到达
  • reqCount:预计请求量,用于计算漏斗每次流出的数量

3. 令牌桶算法

令牌桶算法的特点是以一个固定的速率不断产生令牌,并将令牌放入到桶中,访问时若桶为空,则表示请求数超限。令牌桶算法实现代码如下:

public boolean isAllowed(String key, int capacity, double rate, int reqCount) {
    long nowTime = System.currentTimeMillis();
    Jedis jedis = getJedis();
    String luaScript =
          "local currentLimit = tonumber(redis.call('get', KEYS[1]) or '0')\n"
        + "if currentLimit + ARGV[1] > tonumber(KEYS[2]) then\n"
        + "    return false\n"
        + "else\n"
        + "    redis.call('incrby', KEYS[1], ARGV[1])\n"
        + "    redis.call('expire', KEYS[1], ARGV[2])\n"
        + "    return true\n"
        + "end";
    Object result = jedis.eval(luaScript, 2, key, String.valueOf(capacity), String.valueOf(reqCount), String.valueOf(rate * (nowTime / 1000)));
    boolean allowed = (result instanceof Boolean ? (Boolean) result : false);
    jedis.close();
    return allowed;
}
  • key:需要进行限流的用户标识
  • capacity:桶容量
  • rate:令牌发放速率
  • reqCount:请求数量

四、分布式限流实战

1. 单机限流实现

假设我们有一个需求,需要限制每个IP一分钟内最多只能发送100个请求。可以通过Redis的INCR、EXPIRE等API操作来简单实现单机限流。

public boolean isAllowed(String ip, int limit, int interval) {
    Jedis jedis = getJedis();
    String key = "ip:" + ip;
    long count = jedis.incr(key);
    if (count == 1) {
        jedis.expire(key, interval);
    }
    boolean allowed = count <= limit;
    if (!allowed) {
        jedis.del(key);
    }
    jedis.close();
    return allowed;
}

2. 基于Redis Clusters的分布式限流实现

当业务规模扩大时,单机的限流已经无法满足需求,这时候需要考虑使用Redis Clusters实现分布式限流。Clusers扩展了原先Redis的功能,不仅支持横向扩展,而且提高了整个集群的可用性。限流算法同上,只是需要使用把数据分配到Cluser内不同的节点上。

public boolean isAllowed(String ip, int limit, int interval) {
    JedisCluster jedis = getJedisCluster();
    String key = "ip:" + ip;
    long count = jedis.incr(key);
    if (count == 1) {
        jedis.expire(key, interval);
    }
    boolean allowed = count <= limit;
    if (!allowed) {
        jedis.del(key);
    }
    return allowed;
}

五、基于Redis分布式限流的优化

1. 缓存击穿

1.1 问题描述

在高并发场景下,如果存在大量的缓存未命中请求,将会导致访问底层数据存储系统,这种情况被称为缓存击穿

1.2 解决方案

1.2.1 使用互斥锁
/**
 * 获取缓存值方法
 * @param key 缓存键值
 * @return 缓存值
 */
public String getCacheValue(String key) {
    String value = cache.get(key);
    if (value == null) { //缓存未命中
        //使用互斥锁
        Lock lock = redisson.getLock(key);
        if (lock.tryLock()) { //尝试获取锁
            try {
                value = cache.get(key); //再次尝试获取缓存
                if (value == null) { //如果仍然未命中,从数据库中获取
                    value = db.get(key);
                    cache.put(key, value); //将查询结果放入缓存
                }
            } finally {
                lock.unlock(); //释放锁
            }
        } else {
            Thread.sleep(100); //自旋一段时间后重试
            return getCacheValue(key);
        }
    }
    return value;
}
1.2.2 使用预热机制

预热机制是指在系统启动的时候,提前加载热点数据到缓存中,以减少缓存未命中请求。预热的方式可以使用定时任务或者其他方式,在系统低峰期进行加载。

2. 热点key问题的解决方案

2.1 问题描述

在高并发场景下,如果某个key的请求量过大,将会导致这个key成为热点key,从而导致缓存雪崩问题。

2.2 解决方案

2.2.1 分布式锁
/**
 * 获取缓存值方法
 * @param key 缓存键值
 * @return 缓存值
 */
public String getCacheValue(String key) {
    String value = cache.get(key);
    if (value == null) { //缓存未命中
        //使用分布式锁
        RLock lock = redisson.getFairLock(key);
        if (lock.tryLock()) { //尝试获取锁
            try {
                value = cache.get(key); //再次尝试获取缓存
                if (value == null) { //如果仍然未命中,从数据库中获取
                    value = db.get(key);
                    cache.put(key, value); //将查询结果放入缓存
                }
            } finally {
                lock.unlock(); //释放锁
            }
        } else {
            Thread.sleep(100); //自旋一段时间后重试
            return getCacheValue(key);
        }
    }
    return value;
}
2.2.2 分布式缓存

使用分布式缓存将数据均匀地分散到多个节点上,从而避免单点瓶颈问题。

3. 并发竞争优化

3.1 问题描述

在高并发场景下,对于某些资源的并发访问将会导致性能瓶颈,需要进行并发竞争优化。

3.2 解决方案

3.2.1 使用限流器

限流器类似于信号灯,用于控制并发请求的数量,在高峰期可以采用漏桶算法或令牌桶算法进行限流。

3.2.2 使用异步线程池

对于一些耗时的操作,可以使用异步线程池进行处理,从而避免阻塞主线程,提升系统的并发能力。文章来源地址https://www.toymoban.com/news/detail-657543.html

到了这里,关于基于 Redis 实现分布式限流的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 2、基于redis实现分布式锁

    借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。 多个客户端同时获取锁(setnx) 获取成功,执行业务逻辑,执行完成释放锁(del) 其他客户端等

    2024年02月15日
    浏览(49)
  • 基于 SpringBoot + Redis 实现分布式锁

    大家好,我是余数,这两天温习了下分布式锁,然后就顺便整理了这篇文章出来。 文末附有源码链接,需要的朋友可以自取。 至于什么是分布式锁,这里不做赘述,不了解的可以自行去查阅资料。 1. 使用 Redis 的 Setnx(SET if Not Exists) 命令加锁。 即锁不存在的时候才能加锁成功

    2024年02月05日
    浏览(45)
  • 自定义注解,基于redis实现分布式锁

    1.1、注解的基础知识 实现自定义注解其实很简单,格式基本都差不多。也就参数可能变一变。 @Retention:取值决定了注解在什么时候生效,一般都是取运行时,也就是RetentionPolicy.RUNTIME。 @Target:决定了这个注解可以使用在哪些地方,可以取方法,字段,类等。 注解这就定义

    2024年02月08日
    浏览(28)
  • 【Redis从入门到进阶】第 7 讲:基于 Redis 实现分布式锁

    本文已收录于专栏 🍅《Redis从入门到进阶》🍅    本专栏开启,目的在于帮助大家更好的掌握学习 Redis ,同时也是为了记录我自己学习 Redis 的过程,将会从基础的数据类型开始记录,直到一些更多的应用,如缓存击穿还有分布式锁等。希望大家有问题也可以一起沟通,欢

    2023年04月26日
    浏览(30)
  • Zookeeper 和 Redis 哪种更好? 为什么使用分布式锁? 1. 利用 Redis 提供的 第二种,基于 ZK 实现分布式锁的落地方案 对于 redis 的分布式锁而言,它有以下缺点:

    关于这个问题,我们 可以从 3 个方面来说: 为什么使用分布式锁? 使用分布式锁的目的,是为了保证同一时间只有一个 JVM 进程可以对共享资源进行操作。 根据锁的用途可以细分为以下两类: 允许多个客户端操作共享资源,我们称为共享锁 这种锁的一般是对共享资源具有

    2024年01月16日
    浏览(39)
  • Redis实战案例14-分布式锁的基本原理、不同实现方法对比以及基于Redis进行实现思路

    基于数据库的分布式锁:这种方式使用数据库的特性来实现分布式锁。具体流程如下: 获取锁:当一个节点需要获得锁时,它尝试在数据库中插入一个特定的唯一键值(如唯一约束的主键),如果插入成功,则表示获得了锁。 释放锁:当节点完成任务后,通过删除该唯一键

    2024年02月13日
    浏览(41)
  • 基于 Redis + Lua 脚本实现分布式锁,确保操作的原子性

    1.加锁的Lua脚本: lock.lua 2.解锁的Lua脚本: unLock.lua 3.将资源文件放在资源文件夹下 4.Java中调用lua脚本 1)获取文件方式 2)lua字符串方式 5.jedis调用Lua脚本实现分布式重试锁 1)引入jedis依赖 2)jedis调用lua

    2024年02月07日
    浏览(41)
  • 一种基于springboot、redis的分布式任务引擎的实现(一)

     总体思路是,主节点接收到任务请求,将根据任务情况拆分成多个任务块,将任务块标识的主键放入redis。发送redis消息,等待其他节点运行完毕,结束处理。接收到信息的节点注册本节点信息到redis、开启多线程、获取任务块、执行任务、结束处理。 1、主节点接收任务请求

    2024年02月11日
    浏览(28)
  • Asynq: 基于Redis实现的Go生态分布式任务队列和异步处理库

    Asynq [1] 是一个Go实现的分布式任务队列和异步处理库,基于redis,类似Ruby的 sidekiq [2] 和Python的 celery [3] 。Go生态类似的还有 machinery [4] 和goworker 同时提供一个WebUI asynqmon [5] ,可以源码形式安装或使用Docker image, 还可以和Prometheus集成 docker run --rm --name asynqmon -p 8080:8080 hibiken/as

    2024年02月14日
    浏览(34)
  • Python爬虫分布式架构 - Redis/RabbitMQ工作流程介绍

    在大规模数据采集和处理任务中,使用分布式架构可以提高效率和可扩展性。本文将介绍Python爬虫分布式架构中常用的消息队列工具Redis和RabbitMQ的工作流程,帮助你理解分布式爬虫的原理和应用。 为什么需要分布式架构? 在数据采集任务中,单机爬虫可能面临性能瓶颈和资

    2024年02月11日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包