写点什么

Java Redis 多限流

  • 2024-09-26
    福建
  • 本文字数:6317 字

    阅读完需:约 21 分钟

在 Java 中实现 Redis 多限流通常涉及使用 Redis 的某些特性,如INCREXPIRELua脚本或者更高级的 Redis 数据结构如Redis BitmapsRedis Streams结合Redis Pub/Sub,或者使用 Redis 的第三方库如Redis Rate Limiter(基于 Lua 脚本或 Redis 自身功能实现)。然而,为了直接和易于实现,这里我们将使用Jedis库(Java 的 Redis 客户端)结合 Redis 的INCREXPIRE命令来模拟一个基本的分布式多限流系统。


1. 使用Jedis库结合 Redis 的INCREXPIRE命令模拟一个基本的分布式多限流系统


1.1 准备工作


(1)Redis 安装:确保 Redis 服务在我们的开发环境中已经安装并运行。


(2)Jedis 依赖:在我们的 Java 项目中添加 Jedis 依赖。如果我们使用 Maven,可以在pom.xml中添加以下依赖:


<dependency>      <groupId>redis.clients</groupId>      <artifactId>jedis</artifactId>      <version>最新版本</version>  </dependency>
复制代码


请替换最新版本为当前 Jedis 的最新版本。


1.2 实现代码


下面是一个简单的 Java 程序,使用 Jedis 和 Redis 的INCREXPIRE命令来实现基本的限流功能。这里我们假设每个用户(或 API 端点)都有自己的限流键。


import redis.clients.jedis.Jedis;    public class RedisRateLimiter {        private static final String REDIS_HOST = "localhost";      private static final int REDIS_PORT = 6379;      private static final long LIMIT = 10; // 每分钟最多请求次数      private static final long TIME_INTERVAL = 60; // 时间间隔,单位为秒        public static void main(String[] args) {          try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {              String userId = "user123"; // 假设这是用户ID或API端点标识符              String key = "rate_limit:" + userId;                // 尝试获取访问权限              if (tryAcquire(jedis, key, LIMIT, TIME_INTERVAL)) {                  System.out.println("请求成功,未超过限流限制");                  // 在这里处理你的请求                } else {                  System.out.println("请求失败,超过限流限制");                  // 处理限流情况,如返回错误码或等待一段时间后重试              }            } catch (Exception e) {              e.printStackTrace();          }      }        /**       * 尝试获取访问权限       *       * @param jedis Redis客户端       * @param key   限流键       * @param limit 限制次数       * @param timeInterval 时间间隔(秒)       * @return 是否获取成功       */      public static boolean tryAcquire(Jedis jedis, String key, long limit, long timeInterval) {          String result = jedis.watch(key);          if (result != null && result.equalsIgnoreCase("OK")) {              String counter = jedis.get(key);              if (counter == null || Long.parseLong(counter) < limit) {                  // 使用事务,先incr后expire,确保原子性                  Transaction transaction = jedis.multi();                  transaction.incr(key);                  transaction.expire(key, timeInterval);                  List<Object> results = transaction.exec();                  if (results != null && results.size() == 2 && "OK".equals(results.get(0).toString()) && "1".equals(results.get(1).toString())) {                      return true;                  }              }              // 取消watch              jedis.unwatch();          }          // 如果key不存在或超过限制,则直接返回false          return false;      }  }
复制代码


注意:上述代码中的tryAcquire方法使用了 Redis 的WATCHMULTI/EXEC命令来尝试实现操作的原子性,但这种方法在 Redis 集群环境中可能不是最佳实践,因为WATCH/UNWATCH是基于单个 Redis 实例的。在分布式环境中,我们可能需要考虑使用 Redis 的 Lua 脚本来确保操作的原子性,或者使用专门的限流库。


此外,上述代码在并发极高的情况下可能不是最优的,因为它依赖于 Redis 的WATCH机制来避免竞态条件,这在性能上可能不是最高效的。对于高并发的限流需求,我们可能需要考虑使用更专业的限流算法或库,如令牌桶(Token Bucket)或漏桶(Leaky Bucket)。


2. 基于 Jedis 和 Lua 脚本的限流示例


在 Java 中使用 Redis 进行多限流时,我们通常会选择更健壮和高效的方案,比如使用 Redis 的 Lua 脚本来保证操作的原子性,或者使用现成的 Redis 限流库。不过,为了保持示例的简洁性和易于理解,我将提供一个基于 Jedis 和 Lua 脚本的限流示例。


在这个示例中,我们将使用 Redis 的 Lua 脚本来实现一个简单的令牌桶限流算法。Lua 脚本可以在 Redis 服务器上以原子方式执行多个命令,这对于限流等需要原子操作的场景非常有用。


2.1 Java Redis 多限流(Lua 脚本示例)


首先,我们需要有一个 Redis 服务器运行在我们的环境中,并且我们的 Java 项目中已经添加了 Jedis 依赖。


2.1.1 Lua 脚本


以下是一个简单的 Lua 脚本,用于实现令牌桶的限流逻辑。这个脚本会检查当前桶中的令牌数,如果足够则减少令牌数并返回成功,否则返回失败。


-- Lua脚本:token_bucket_limit.lua  -- KEYS[1] 是令牌桶的key  -- ARGV[1] 是请求的令牌数  -- ARGV[2] 是桶的容量  -- ARGV[3] 是每秒添加的令牌数  -- ARGV[4] 是时间间隔(秒),用于计算当前时间应该有多少令牌    local key = KEYS[1]  local request = tonumber(ARGV[1])  local capacity = tonumber(ARGV[2])  local rate = tonumber(ARGV[3])  local interval = tonumber(ARGV[4])    -- 获取当前时间戳  local current_time = tonumber(redis.call("TIME")[1])    -- 尝试获取桶的上次更新时间和当前令牌数  local last_updated_time = redis.call("GET", key .. "_last_updated_time")  local current_tokens = redis.call("GET", key .. "_tokens")    if last_updated_time == false then      -- 如果桶不存在,则初始化桶      redis.call("SET", key .. "_last_updated_time", current_time)      redis.call("SET", key .. "_tokens", capacity)      current_tokens = capacity      last_updated_time = current_time  end    -- 计算自上次更新以来经过的时间  local delta = current_time - last_updated_time    -- 计算这段时间内应该添加的令牌数  local tokens_to_add = math.floor(delta * rate)    -- 确保令牌数不会超过容量  if current_tokens + tokens_to_add > capacity then      tokens_to_add = capacity - current_tokens  end    -- 更新令牌数和更新时间  current_tokens = current_tokens + tokens_to_add  redis.call("SET", key .. "_tokens", current_tokens)  redis.call("SET", key .. "_last_updated_time", current_time)    -- 检查是否有足够的令牌  if current_tokens >= request then      -- 如果有足够的令牌,则减少令牌数      redis.call("DECRBY", key .. "_tokens", request)      return 1  -- 返回成功  else      return 0  -- 返回失败  end
复制代码


2.1.2 Java 代码


接下来是 Java 中使用 Jedis 调用上述 Lua 脚本的代码。


import redis.clients.jedis.Jedis;    public class RedisRateLimiter {        private static final String REDIS_HOST = "localhost";      private static final int REDIS_PORT = 6379;      private static final String LUA_SCRIPT = "path/to/your/token_bucket_limit.lua"; // Lua脚本的路径(或者你可以直接加载脚本内容)        public static void main(String[] args) {          try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {              String key = "rate_limit_bucket:user123";              int requestTokens = 1;              int capacity = 10;              double rate = 1.0; // 每秒添加1个令牌              int interval = 60; // 时间间隔为60秒                // 加载Lua脚本(这里假设你已经有了Lua脚本的内容或路径)              // 实际应用中,你可能需要从文件加载Lua脚本内容              String scriptContent = // ... 从文件或其他地方加载Lua脚本内容                // 注册Lua脚本到Redis              String sha1 = jedis.scriptLoad(scriptContent);                // 执行Lua脚本              Object result = jedis.evalsha(sha1, 1, key, String.valueOf(requestTokens), String.valueOf(capacity), String.
复制代码


在之前的代码中,我们留下了加载 Lua 脚本和执行它的部分未完成。以下是完整的 Java 代码示例,包括如何加载 Lua 脚本并执行它以进行限流检查。


2.1.3 完整的 Java 代码示例


import redis.clients.jedis.Jedis;    import java.io.BufferedReader;  import java.io.FileReader;  import java.io.IOException;    public class RedisRateLimiter {        private static final String REDIS_HOST = "localhost";      private static final int REDIS_PORT = 6379;      private static final String LUA_SCRIPT_PATH = "path/to/your/token_bucket_limit.lua"; // Lua脚本的文件路径        public static void main(String[] args) {          try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {              String key = "rate_limit_bucket:user123";              int requestTokens = 1;              int capacity = 10;              double rate = 1.0; // 每秒添加1个令牌              int interval = 1; // 时间间隔为1秒(这里仅为示例,实际中可能更长)                // 加载Lua脚本              String luaScript = loadLuaScript(LUA_SCRIPT_PATH);                // 注册Lua脚本到Redis(获取SHA1哈希值)              String sha1 = jedis.scriptLoad(luaScript);                // 执行Lua脚本进行限流检查              // KEYS[1] 是 key, ARGV 是其他参数              Long result = (Long) jedis.evalsha(sha1, 1, key, String.valueOf(requestTokens), String.valueOf(capacity), String.valueOf(rate), String.valueOf(interval));                if (result == 1L) {                  System.out.println("请求成功,有足够的令牌。");                  // 处理请求...              } else {                  System.out.println("请求失败,令牌不足。");                  // 拒绝请求或进行其他处理...              }            } catch (Exception e) {              e.printStackTrace();          }      }        // 从文件加载Lua脚本内容      private static String loadLuaScript(String filePath) throws IOException {          StringBuilder sb = new StringBuilder();          try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {              String line;              while ((line = reader.readLine()) != null) {                  sb.append(line).append("\n");              }          }          return sb.toString();      }  }
复制代码


2.1.4 注意事项


(1)Lua 脚本路径:确保LUA_SCRIPT_PATH变量指向正确的 Lua 脚本文件路径。


(2)错误处理:在实际应用中,我们可能需要添加更详细的错误处理逻辑,比如处理 Redis 连接失败、Lua 脚本加载失败等情况。


(3)性能考虑:虽然 Lua 脚本在 Redis 中执行是高效的,但在高并发场景下,频繁的脚本执行仍然可能对 Redis 服务器造成压力。我们可能需要考虑使用 Redis 的内置限流功能(如 Redis 6.0 及以上版本的Redis StreamsRedis Bloom Filters),或者通过增加 Redis 实例、使用集群等方式来扩展我们的系统。


(4)Lua 脚本的复杂性:随着业务逻辑的复杂化,Lua 脚本可能会变得难以维护。在这种情况下,我们可能需要考虑将部分逻辑移到 Java 代码中,或者通过其他方式(如使用 Redis 的模块)来扩展 Redis 的功能。


(5)时间同步:Lua 脚本中的时间计算依赖于 Redis 服务器的时间。确保 Redis 服务器的时间与我们的应用服务器时间保持同步,以避免因时间差异导致的问题。


3. Redis 多限流


Redis 作为一种高性能的键值对存储系统,支持多种数据结构和操作,非常适合用于实现限流算法。以下是关于 Redis 多限流的一些详细信息:


3.1 Redis 限流算法概述


Redis 实现限流主要依赖于其原子操作、高速缓存和丰富的数据结构(如字符串、列表、集合、有序集合等)。常见的限流算法包括令牌桶算法(Token Bucket)、漏桶算法(Leaky Bucket)以及基于计数器的简单限流算法。


(1)令牌桶算法:

  • 初始化一个固定容量的令牌桶,以固定速率向桶中添加令牌。

  • 每个请求尝试从桶中获取一个令牌,如果成功则处理请求,否则拒绝或等待。

  • 令牌桶的容量和添加速率决定了系统的最大处理能力和平均处理速率。


(2)漏桶算法:

  • 请求被放入一个桶中,桶以恒定速率漏出请求。

  • 如果桶满,则新到的请求被拒绝或等待。

  • 漏桶算法对突发流量有很好的抑制作用,但可能无法高效利用资源。


(3)计数器算法:

  • 在每个时间窗口内记录请求次数,达到阈值时拒绝新请求。

  • 时间窗口结束后计数器重置。

  • 实现简单但可能存在临界问题,限流不准确。


3.2 Redis 多限流实现方式


在分布式系统中,Redis 可以实现全局的限流,支持多种限流策略的组合使用。


(1)使用 Redis 数据结构:

  • 字符串:记录当前时间窗口内的请求次数或令牌数。

  • 列表:记录请求的时间戳,用于滑动窗口算法。

  • 有序集合(ZSet):记录请求的时间戳和唯一标识,用于精确控制时间窗口内的请求数。

  • 哈希表:存储令牌桶的状态,包括当前令牌数和上次更新时间。


(2)Lua 脚本:

  • 利用 Redis 的 Lua 脚本功能,可以编写复杂的限流逻辑,并通过原子操作执行,确保并发安全性。

  • Lua 脚本可以在 Redis 服务器端执行,减少网络传输和延迟。


(3)分布式锁:

  • 在高并发场景下,为了防止多个实例同时修改同一个限流键,可以使用 Redis 的分布式锁机制。

  • 但需要注意分布式锁的性能和可用性问题。


3.3 Redis 多限流实际应用


在实际应用中,Redis 多限流可以用于多种场景,如 API 接口限流、用户行为限流、系统资源访问限流等。通过组合不同的限流算法和数据结构,可以实现复杂的限流策略,满足不同业务需求。


例如,一个电商平台可能需要对用户登录、商品浏览、下单等行为进行限流。对于登录行为,可以使用令牌桶算法限制用户登录频率;对于商品浏览行为,可以使用漏桶算法控制突发流量;对于下单行为,则可能需要结合用户身份、订单金额等多个因素进行综合限流。


3.4 注意事项


(1)性能问题:在高并发场景下,Redis 的性能可能会成为瓶颈。需要合理设计限流策略和 Redis 的部署架构,确保系统稳定运行。


(2)持久化问题:Redis 是内存数据库,数据丢失风险较高。在需要持久化限流数据的场景下,需要考虑 Redis 的持久化机制。


(3)分布式问题:在分布式系统中,需要确保 Redis 集群的稳定性和可用性,以及限流数据的一致性和准确性。


综上所述,Redis 多限流是一种强大而灵活的技术手段,通过合理的策略设计和实现方式,可以有效地保护系统资源和服务质量。


文章转载自:TechSynapse

原文链接:https://www.cnblogs.com/TS86/p/18297887

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
Java Redis多限流_Java_快乐非自愿限量之名_InfoQ写作社区