使用Redis + lua脚本实现分布式限流

源码地址https://gitee.com/mr_wenpan/basis-enhance/tree/master/enhance-boot-limiting

一、功能介绍

  • 该项目(enhance-boot-limiting)主要是基于Redis + lua实现了分布式限流功能
  • 项目中提供两种分布式限流算法(一种是滑动时间窗口算法、一种是令牌桶算法)
  • 项目中提供了方便使用的注解形式来直接对接口进行限流,详情见@SlideWindowLimit@TokenBucketLimit注解,并且提供了@EnableRedisLimiting注解来实现动态可插拔功能
  • 同时项目中也提供了灵活使用的限流助手RedisLimitHelper,可以通过RedisLimitHelper来灵活的实现限流功能

二、如何使用

1、拉取项目源代码

  • 源码地址:https://gitee.com/mr_wenpan/basis-enhance.git
  • 由于我是将多个项目通过一个父pom来进行管理,并且jar包没有发布到maven仓库,所以需要自己将enhance-boot-limiting模块打包到自己本地maven私服(打包命令:mvn clean install

2、打包项目到maven私服

  • 命令 mvn clean install

3、项目的pom文件中引入enhance-boot-limiting依赖

  • 注意:由于该组件是基于Redis而开发的分布式限流器,所以需要依赖spring-boot-starter-data-redis模块
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.basis.enhance</groupId>
    <artifactId>enhance-boot-limiting</artifactId>
    <version>1.0-SNAPSHOT</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
  </dependency>
</dependencies>

4、application.yml文件中配置

server:
  port: 20002
spring:
  application:
    name: enhance-data-redis-demo
  # redis配置
  redis:
    host: ${SPRING_REDIS_HOST:192.168.1.22}
    port: ${SPRING_REDIS_PORT:6379}
    password: ${SPRING_REDIS_PASSWORD:123456}
    database: ${SPRING_REDIS_DATABASE:8}
    client-type: lettuce
    lettuce:
      pool:
        max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:16}
        max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:16}
        max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}

5、主启动类上开启限流功能

  • 使用@EnableRedisLimiting显示开启限流功能
// 开启分布式限流
@EnableRedisLimiting
@SpringBootApplication
@EnableConfigurationProperties
public class DemoEnhanceLimitApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoEnhanceLimitApplication.class, args);
    }

}

三、使用测试

1、使用限流注解

直接在需要限流的接口上使用@TokenBucketLimit@SlideWindowLimit便可以进行限流

/**
* 使用注解进行滑动时间窗限流
*/
@SlideWindowLimit
@GetMapping("/test-annotation")
  public String testAnnotation() {
  log.info("请求没有被限流...");
  return "success";
}

/**
 * 使用注解进行令牌桶限流
 */
@TokenBucketLimit
@GetMapping("/test-annotation")
public String testAnnotation() {
  log.info("请求没有被限流...");
  return "success";
}

2、使用redisLimitHelper进行限流

  • 使用redisLimitHelper进行限流更加灵活
 @GetMapping("/test-01")
public String test01(String limitKey,Integer capacity,Integer permits,Double rate) {
  // 循环100次
  for (int i = 0; i < 100; i++) {
    // 桶的容量为10,每秒流入1个令牌,每次获取一个令牌
    Boolean limit = redisLimitHelper.tokenLimit(limitKey, capacity, permits, rate);
    // 是否被限流
    if (limit) {
      log.info("[{}] pass.", i);
    } else {
      log.error("[{}] can not pass.", i);
    }
  }
  return "success";
}

3、使用示例

四、核心原理

使用方式和原理都很简单,不过多赘述,这里贴出两个核心的lua脚本

1、滑动窗口限流lua脚本

-- key对应着某个接口, value对应着这个接口的上一次请求时间
local unique_identifier = KEYS[1]
-- 上次请求时间key
local timeKey = 'lastTime'
-- 时间窗口内累计请求数量key
local requestKey = 'requestCount'
-- 限流大小,限流最大请求数
local maxRequest = tonumber(ARGV[1])
-- 当前请求时间戳,也就是请求的发起时间(毫秒)
local nowTime = tonumber(ARGV[2])
-- 窗口长度(毫秒)
local windowLength = tonumber(ARGV[3])

-- 限流开始时间
local currentTime = tonumber(redis.call('HGET', unique_identifier, timeKey) or '0')
-- 限流累计请求数
local currentRequest = tonumber(redis.call('HGET', unique_identifier, requestKey) or '0')

-- 当前时间在滑动窗口内
if currentTime + windowLength > nowTime then
    if currentRequest + 1 > maxRequest then
        return 0;
    else
        -- 在时间窗口内且请求数没超,请求数加一
        redis.call('HINCRBY', unique_identifier, requestKey, 1)
        return 1;
    end
else
    -- 超时后重置,开启一个新的时间窗口
    redis.call('HSET', unique_identifier, timeKey, nowTime)
    redis.call('HSET', unique_identifier, requestKey, '0')
    -- 窗口过期时间
    local expireTime = windowLength / 1000;
    redis.call('EXPIRE', unique_identifier, expireTime)
    redis.call('HINCRBY', unique_identifier, requestKey, 1)
    return 1;
end

2、令牌桶限流lua脚本

-- 令牌桶
local bucketKey = KEYS[1]
-- 上次请求的时间key
local last_request_time_key = 'lastRequestTime'
-- 令牌桶的容量
local capacity = tonumber(ARGV[1])
-- 请求令牌的数量
local permits = tonumber(ARGV[2])
-- 令牌流入的速率(按毫秒计算)
local rate = tonumber(ARGV[3])
-- 当前时间(毫秒)
local current_time = tonumber(ARGV[4])
-- 唯一标识
local unique_identifier = bucketKey

-- 恶意请求
if permits <= 0 then
    return 1
end

-- 获取当前桶内令牌的数量
local current_limit = tonumber(redis.call('HGET', unique_identifier, bucketKey) or '0')
-- 获取上次请求的时间
local last_mill_request_time = tonumber(redis.call('HGET', unique_identifier, last_request_time_key) or '0')
-- 计算向桶里添加令牌的数量
local add_token_num = 0
if last_mill_request_time == 0 then
   -- 如果是第一次请求,则进行初始化令牌桶,并且更新上次请求时间
   add_token_num = capacity
   redis.call("HSET", unique_identifier, last_request_time_key, current_time)
else
    -- 令牌流入桶内
   add_token_num = math.floor((current_time - last_mill_request_time) * rate)
end

-- 更新令牌的数量
if current_limit + add_token_num > capacity then
    current_limit = capacity
else
   current_limit = current_limit + add_token_num
end
-- 更新桶内令牌的数量
redis.pcall('HSET',unique_identifier, bucketKey, current_limit)

-- 限流判断
if current_limit - permits < 0 then
    -- 达到限流大小
    return 0
else
    -- 没有达到限流大小
   current_limit = current_limit - permits
   redis.pcall('HSET', unique_identifier, bucketKey, current_limit)
   -- 更新上次请求的时间
   redis.call('HSET', unique_identifier, last_request_time_key, current_time)
   return 1
end
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
实现分布式限流可以使用 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 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值