Go+Redis实现常见限流算法的示例代码

限流是项目中经常需要使用到的一种工具,一般用于限制用户的请求的频率,也可以避免瞬间流量过大导致系统崩溃,或者稳定消息处理速率。并且有时候我们还需要使用到分布式限流,常见的实现方式是使用Redis作为中心存储。

这个文章主要是使用Go+Redis实现常见的限流算法,如果需要了解每种限流算法的原理可以阅读文章 Go实现常见的限流算法

下面的代码使用到了go-redis客户端

固定窗口

使用Redis实现固定窗口比较简单,主要是由于固定窗口同时只会存在一个窗口,所以我们可以在第一次进入窗口时使用pexpire命令设置过期时间为窗口时间大小,这样窗口会随过期时间而失效,同时我们使用incr命令增加窗口计数。

因为我们需要在counter==1的时候设置窗口的过期时间,为了保证原子性,我们使用简单的Lua脚本实现。

  1. const fixedwindowLimiterTryAcquireRedisScript = `
  2. — ARGV[1]: 窗口时间大小
  3. — ARGV[2]: 窗口请求上限
  4.  
  5. local window = tonumber(ARGV[1])
  6. local limit = tonumber(ARGV[2])
  7.  
  8. — 获取原始值
  9. local counter = tonumber(redis.call(“get”, KEYS[1]))
  10. if counter == nil then
  11.      counter = 0
  12. end
  13. — 若到达窗口请求上限,请求失败
  14. if counter >= limit then
  15.      return 0
  16. end
  17. — 窗口值+1
  18. redis.call(“incr”, KEYS[1])
  19. if counter == 0 then
  20.      redis.call(“pexpire”, KEYS[1], window)
  21. end
  22. return 1
  23. `
  1. package redis
  2.  
  3. import (
  4.      “context”
  5.      “errors”
  6.      github.com/go-redis/redis/v8″
  7.      “time”
  8. )
  9.  
  10. // FixedWindowLimiter 固定窗口限流器
  11. type FixedWindowLimiter struct {
  12.      limit int // 窗口请求上限
  13.      window int // 窗口时间大小
  14.      client *redis.Client // Redis客户端
  15.      script *redis.Script // TryAcquire脚本
  16. }
  17.  
  18. func NewFixedWindowLimiter(client *redis.Client, limit int, window time.Duration) (*FixedWindowLimiter, error) {
  19.      // redis过期时间精度最大到毫秒,因此窗口必须能被毫秒整除
  20.      if window%time.Millisecond != 0 {
  21.          return nil, errors.New(“the window uint must not be less than millisecond”)
  22.      }
  23.  
  24.      return &FixedWindowLimiter{
  25.          limit: limit,
  26.          window: int(window / time.Millisecond),
  27.          client: client,
  28.          script: redis.NewScript(fixedWindowLimiterTryAcquireRedisScript),
  29.      }, nil
  30. }
  31.  
  32. func (*FixedWindowLimiter) TryAcquire(ctx context.Context, resource string) error {
  33.      success, err := l.script.Run(ctx, l.client, []string{resource}, l.window, l.limit).Bool()
  34.      if err != nil {
  35.          return err
  36.      }
  37.      // 若到达窗口请求上限,请求失败
  38.      if !success {
  39.          return ErrAcquireFailed
  40.      }
  41.      return nil
  42. }

滑动窗口

hash实现

我们使用Redis的hash存储每个小窗口的计数,每次请求会把所有有效窗口的计数累加到count,使用hdel删除失效窗口,最后判断窗口的总计数是否大于上限。

我们基本上把所有的逻辑都放到Lua脚本里面,其中大头是对hash的遍历,时间复杂度是O(N),N是小窗口数量,所以小窗口数量最好不要太多。

  1. const slidingWindowLimiterTryAcquireRedisScriptHashImpl = `
  2. — ARGV[1]: 窗口时间大小
  3. — ARGV[2]: 窗口请求上限
  4. — ARGV[3]: 当前小窗口值
  5. — ARGV[4]: 起始小窗口值
  6.  
  7. local window = tonumber(ARGV[1])
  8. local limit = tonumber(ARGV[2])
  9. local currentSmallWindow = tonumber(ARGV[3])
  10. local startSmallWindow = tonumber(ARGV[4])
  11.  
  12. — 计算当前窗口的请求总数
  13. local counters = redis.call(“hgetall”, KEYS[1])
  14. local count = 0
  15. for i = 1, #(counters) / 2 do
  16.      local smallWindow = tonumber(counters[i * 2 – 1])
  17.      local counter = tonumber(counters[i * 2])
  18.      if smallWindow < startSmallWindow then
  19.          redis.call(“hdel”, KEYS[1], smallWindow)
  20.      else
  21.          count = count + counter
  22.      end
  23. end
  24.  
  25. — 若到达窗口请求上限,请求失败
  26. if count >= limit then
  27.      return 0
  28. end
  29.  
  30. — 若没到窗口请求上限,当前小窗口计数器+1,请求成功
  31. redis.call(“hincrby”, KEYS[1], currentSmallWindow, 1)
  32. redis.call(“pexpire”, KEYS[1], window)
  33. return 1
  34. `
  1. package redis
  2.  
  3. import (
  4.      “context”
  5.      “errors”
  6.      “github.com/go-redis/redis/v8”
  7.      “time”
  8. )
  9.  
  10. // SlidingWindowLimiter 滑动窗口限流器
  11. type SlidingWindowLimiter struct {
  12.      limit int // 窗口请求上限
  13.      window int64 // 窗口时间大小
  14.      smallWindow int64 // 小窗口时间大小
  15.      smallWindows int64 // 小窗口数量
  16.      client *redis.Client // Redis客户端
  17.      script *redis.Script // TryAcquire脚本
  18. }
  19.  
  20. func NewSlidingWindowLimiter(client *redis.Client, limit int, window, smallWindow time.Duration) (
  21.      *SlidingWindowLimiter, error) {
  22.      // redis过期时间精度最大到毫秒,因此窗口必须能被毫秒整除
  23.      if window%time.Millisecond != 0 || smallWindow%time.Millisecond != 0 {
  24.          return nil, errors.New(“the window uint must not be less than millisecond”)
  25.      }
  26.  
  27.      // 窗口时间必须能够被小窗口时间整除
  28.      if window%smallWindow != 0 {
  29.          return nil, errors.New(“window cannot be split by integers”)
  30.      }
  31.  
  32.      return &SlidingWindowLimiter{
  33.          limit: limit,
  34.          window: int64(window / time.Millisecond),
  35.          smallWindow: int64(smallWindow / time.Millisecond),
  36.          smallWindows: int64(window / smallWindow),
  37.          client: client,
  38.          script: redis.NewScript(slidingWindowLimiterTryAcquireRedisScriptHashImpl),
  39.      }, nil
  40. }
  41.  
  42. func (*SlidingWindowLimiter) TryAcquire(ctx context.Context, resource string) error {
  43.      // 获取当前小窗口值
  44.      currentSmallWindow := time.Now().UnixMilli() / l.smallWindow * l.smallWindow
  45.      // 获取起始小窗口值
  46.      startSmallWindow := currentSmallWindow  l.smallWindow*(l.smallWindows1)
  47.  
  48.      success, err := l.script.Run(
  49.          ctx, l.client, []string{resource}, l.window, l.limit, currentSmallWindow, startSmallWindow).Bool()
  50.      if err != nil {
  51.          return err
  52.      }
  53.      // 若到达窗口请求上限,请求失败
  54.      if !success {
  55.          return ErrAcquireFailed
  56.      }
  57.      return nil
  58. }

list实现

如果小窗口数量特别多,可以使用list优化时间复杂度,list的结构是:

[counter, smallWindow1, count1, smallWindow2, count2, smallWindow3, count3...]

也就是我们使用list的第一个元素存储计数器,每个窗口用两个元素表示,第一个元素表示小窗口值,第二个元素表示这个小窗口的计数。不直接把小窗口值和计数放到一个元素里是因为Redis Lua脚本里没有分割字符串的函数。

具体操作流程:

1.获取list长度

2.如果长度是0,设置counter,长度+1

3.如果长度大于1,获取第二第三个元素

如果该值小于起始小窗口值,counter-第三个元素的值,删除第二第三个元素,长度-2

4.如果counter大于等于limit,请求失败

5.如果长度大于1,获取倒数第二第一个元素

  • 如果倒数第二个元素小窗口值大于等于当前小窗口值,表示当前请求因为网络延迟的问题,到达服务器的时候,窗口已经过时了,把倒数第二个元素当成当前小窗口(因为它更新),倒数第一个元素值+1
  • 否则,添加新的窗口值,添加新的计数(1),更新过期时间

6.否则,添加新的窗口值,添加新的计数(1),更新过期时间

7.counter + 1

8.返回成功

  1. const slidingWindowLimiterTryAcquireRedisScriptListImpl = `
  2. — ARGV[1]: 窗口时间大小
  3. — ARGV[2]: 窗口请求上限
  4. — ARGV[3]: 当前小窗口值
  5. — ARGV[4]: 起始小窗口值
  6.  
  7. local window = tonumber(ARGV[1])
  8. local limit = tonumber(ARGV[2])
  9. local currentSmallWindow = tonumber(ARGV[3])
  10. local startSmallWindow = tonumber(ARGV[4])
  11.  
  12. — 获取list长度
  13. local len = redis.call(“llen”, KEYS[1])
  14. — 如果长度是0,设置counter,长度+1
  15. local counter = 0
  16. if len == 0 then
  17.      redis.call(“rpush”, KEYS[1], 0)
  18.      redis.call(“pexpire”, KEYS[1], window)
  19.      len = len + 1
  20. else
  21.      — 如果长度大于1,获取第二第个元素
  22.      local smallWindow1 = tonumber(redis.call(“lindex”, KEYS[1], 1))
  23.      counter = tonumber(redis.call(“lindex”, KEYS[1], 0))
  24.      — 如果该值小于起始小窗口值
  25.      if smallWindow1 < startSmallWindow then
  26.          local count1 = redis.call(“lindex”, KEYS[1], 2)
  27.          — counter-第三个元素的值
  28.          counter = counter – count1
  29.          — 长度-2
  30.          len = len – 2
  31.          — 删除第二第三个元素
  32.          redis.call(“lrem”, KEYS[1], 1, smallWindow1)
  33.          redis.call(“lrem”, KEYS[1], 1, count1)
  34.      end
  35. end
  36.  
  37. — 若到达窗口请求上限,请求失败
  38. if counter >= limit then
  39.      return 0
  40. end
  41.  
  42. — 如果长度大于1,获取倒数第二第一个元素
  43. if len > 1 then
  44.      local smallWindown = tonumber(redis.call(“lindex”, KEYS[1], -2))
  45.      — 如果倒数第二个元素小窗口值大于等于当前小窗口值
  46.      if smallWindown >= currentSmallWindow then
  47.          — 把倒数第二个元素当成当前小窗口(因为它更新),倒数第一个元素值+1
  48.          local countn = redis.call(“lindex”, KEYS[1], -1)
  49.          redis.call(“lset”, KEYS[1], -1, countn + 1)
  50.      else
  51.          — 否则,添加新的窗口值,添加新的计数(1),更新过期时间
  52.          redis.call(“rpush”, KEYS[1], currentSmallWindow, 1)
  53.          redis.call(“pexpire”, KEYS[1], window)
  54.      end
  55. else
  56.      — 否则,添加新的窗口值,添加新的计数(1),更新过期时间
  57.      redis.call(“rpush”, KEYS[1], currentSmallWindow, 1)
  58.      redis.call(“pexpire”, KEYS[1], window)
  59. end
  60.  
  61. — counter + 1并更新
  62. redis.call(“lset”, KEYS[1], 0, counter + 1)
  63. return 1
  64. `

算法都是操作list头部或者尾部,所以时间复杂度接近O(1)

漏桶算法

漏桶需要保存当前水位和上次放水时间,因此我们使用hash来保存这两个值。

  1. const leakyBucketLimiterTryAcquireRedisScript = `
  2. — ARGV[1]: 最高水位
  3. — ARGV[2]: 水流速度/秒
  4. — ARGV[3]: 当前时间(秒)
  5.  
  6. local peakLevel = tonumber(ARGV[1])
  7. local currentVelocity = tonumber(ARGV[2])
  8. local now = tonumber(ARGV[3])
  9.  
  10. local lastTime = tonumber(redis.call(“hget”, KEYS[1], “lastTime”))
  11. local currentLevel = tonumber(redis.call(“hget”, KEYS[1], “currentLevel”))
  12. — 初始化
  13. if lastTime == nil then
  14.      lastTime = now
  15.      currentLevel = 0
  16.      redis.call(“hmset”, KEYS[1], “currentLevel”, currentLevel, “lastTime”, lastTime)
  17. end
  18.  
  19. — 尝试放水
  20. — 距离上次放水的时间
  21. local interval = now – lastTime
  22. if interval > 0 then
  23.      — 当前水位-距离上次放水的时间(秒)*水流速度
  24.      local newLevel = currentLevel – interval * currentVelocity
  25.      if newLevel < 0 then
  26.          newLevel = 0
  27.      end
  28.      currentLevel = newLevel
  29.      redis.call(“hmset”, KEYS[1], “currentLevel”, newLevel, “lastTime”, now)
  30. end
  31.  
  32. — 若到达最高水位,请求失败
  33. if currentLevel >= peakLevel then
  34.      return 0
  35. end
  36. — 若没有到达最高水位,当前水位+1,请求成功
  37. redis.call(“hincrby”, KEYS[1], “currentLevel”, 1)
  38. redis.call(“expire”, KEYS[1], peakLevel / currentVelocity)
  39. return 1
  40. `
  1. package redis
  2.  
  3. import (
  4.      “context”
  5.      “github.com/go-redis/redis/v8”
  6.      “time”
  7. )
  8.  
  9. // LeakyBucketLimiter 漏桶限流器
  10. type LeakyBucketLimiter struct {
  11.      peakLevel int // 最高水位
  12.      currentVelocity int // 水流速度/秒
  13.      client *redis.Client // Redis客户端
  14.      script *redis.Script // TryAcquire脚本
  15. }
  16.  
  17. func NewLeakyBucketLimiter(client *redis.Client, peakLevel, currentVelocity int) *LeakyBucketLimiter {
  18.      return &LeakyBucketLimiter{
  19.          peakLevel: peakLevel,
  20.          currentVelocity: currentVelocity,
  21.          client: client,
  22.          script: redis.NewScript(leakyBucketLimiterTryAcquireRedisScript),
  23.      }
  24. }
  25.  
  26. func (*LeakyBucketLimiter) TryAcquire(ctx context.Context, resource string) error {
  27.      // 当前时间
  28.      now := time.Now().Unix()
  29.      success, err := l.script.Run(ctx, l.client, []string{resource}, l.peakLevel, l.currentVelocity, now).Bool()
  30.      if err != nil {
  31.          return err
  32.      }
  33.      // 若到达窗口请求上限,请求失败
  34.      if !success {
  35.          return ErrAcquireFailed
  36.      }
  37.      return nil
  38. }

令牌桶

令牌桶可以看作是漏桶的相反算法,它们一个是把水倒进桶里,一个是从桶里获取令牌。

  1. const tokenBucketLimiterTryAcquireRedisScript = `
  2. — ARGV[1]: 容量
  3. — ARGV[2]: 发放令牌速率/秒
  4. — ARGV[3]: 当前时间(秒)
  5.  
  6. local capacity = tonumber(ARGV[1])
  7. local rate = tonumber(ARGV[2])
  8. local now = tonumber(ARGV[3])
  9.  
  10. local lastTime = tonumber(redis.call(“hget”, KEYS[1], “lastTime”))
  11. local currentTokens = tonumber(redis.call(“hget”, KEYS[1], “currentTokens”))
  12. — 初始化
  13. if lastTime == nil then
  14.      lastTime = now
  15.      currentTokens = capacity
  16.      redis.call(“hmset”, KEYS[1], “currentTokens”, currentTokens, “lastTime”, lastTime)
  17. end
  18.  
  19. — 尝试发放令牌
  20. — 距离上次发放令牌的时间
  21. local interval = now – lastTime
  22. if interval > 0 then
  23.      — 当前令牌数量+距离上次发放令牌的时间(秒)*发放令牌速率
  24.      local newTokens = currentTokens + interval * rate
  25.      if newTokens > capacity then
  26.          newTokens = capacity
  27.      end
  28.      currentTokens = newTokens
  29.      redis.call(“hmset”, KEYS[1], “currentTokens”, newTokens, “lastTime”, now)
  30. end
  31.  
  32. — 如果没有令牌,请求失败
  33. if currentTokens == 0 then
  34.      return 0
  35. end
  36. — 果有令牌,当前令牌-1,请求成功
  37. redis.call(“hincrby”, KEYS[1], “currentTokens”, -1)
  38. redis.call(“expire”, KEYS[1], capacity / rate)
  39. return 1
  40. `
  1. package redis
  2.  
  3. import (
  4.      “context”
  5.      “github.com/go-redis/redis/v8”
  6.      “time”
  7. )
  8.  
  9. // TokenBucketLimiter 令牌桶限流器
  10. type TokenBucketLimiter struct {
  11.      capacity int // 容量
  12.      rate int // 发放令牌速率/秒
  13.      client *redis.Client // Redis客户端
  14.      script *redis.Script // TryAcquire脚本
  15. }
  16.  
  17. func NewTokenBucketLimiter(client *redis.Client, capacity, rate int) *TokenBucketLimiter {
  18.      return &TokenBucketLimiter{
  19.          capacity: capacity,
  20.          rate: rate,
  21.          client: client,
  22.          script: redis.NewScript(tokenBucketLimiterTryAcquireRedisScript),
  23.      }
  24. }
  25.  
  26. func (*TokenBucketLimiter) TryAcquire(ctx context.Context, resource string) error {
  27.      // 当前时间
  28.      now := time.Now().Unix()
  29.      success, err := l.script.Run(ctx, l.client, []string{resource}, l.capacity, l.rate, now).Bool()
  30.      if err != nil {
  31.          return err
  32.      }
  33.      // 若到达窗口请求上限,请求失败
  34.      if !success {
  35.          return ErrAcquireFailed
  36.      }
  37.      return nil
  38. }

滑动日志

算法流程与滑动窗口相同,只是它可以指定多个策略,同时在请求失败的时候,需要通知调用方是被哪个策略所拦截。

  1. const slidingLogLimiterTryAcquireRedisScriptHashImpl = `
  2. — ARGV[1]: 当前小窗口值
  3. — ARGV[2]: 第一个策略的窗口时间大小
  4. — ARGV[i * 2 + 1]: 每个策略的起始小窗口值
  5. — ARGV[i * 2 + 2]: 每个策略的窗口请求上限
  6.  
  7. local currentSmallWindow = tonumber(ARGV[1])
  8. — 第一个策略的窗口时间大小
  9. local window = tonumber(ARGV[2])
  10. — 第一个策略的起始小窗口值
  11. local startSmallWindow = tonumber(ARGV[3])
  12. local strategiesLen = #(ARGV) / 2 – 1
  13.  
  14. — 计算每个策略当前窗口的请求总数
  15. local counters = redis.call(“hgetall”, KEYS[1])
  16. local counts = {}
  17. — 初始化counts
  18. for j = 1, strategiesLen do
  19.      counts[j] = 0
  20. end
  21.  
  22. for i = 1, #(counters) / 2 do
  23.      local smallWindow = tonumber(counters[i * 2 – 1])
  24.      local counter = tonumber(counters[i * 2])
  25.      if smallWindow < startSmallWindow then
  26.          redis.call(“hdel”, KEYS[1], smallWindow)
  27.      else
  28.          for j = 1, strategiesLen do
  29.              if smallWindow >= tonumber(ARGV[j * 2 + 1]) then
  30.              counts[j] = counts[j] + counter
  31.              end
  32.          end
  33.      end
  34. end
  35.  
  36. — 若到达对应策略窗口请求上限,请求失败,返回违背的策略下标
  37. for i = 1, strategiesLen do
  38.      if counts[i] >= tonumber(ARGV[i * 2 + 2]) then
  39.          return i – 1
  40.      end
  41. end
  42.  
  43. — 若没到窗口请求上限,当前小窗口计数器+1,请求成功
  44. redis.call(“hincrby”, KEYS[1], currentSmallWindow, 1)
  45. redis.call(“pexpire”, KEYS[1], window)
  46. return -1
  47. `
  1. package redis
  2.  
  3. import (
  4.      “context”
  5.      “errors”
  6.      “fmt”
  7.      “github.com/go-redis/redis/v8”
  8.      “sort”
  9.      “time”
  10. )
  11.  
  12. // ViolationStrategyError 违背策略错误
  13. type ViolationStrategyError struct {
  14.      Limit int // 窗口请求上限
  15.      Window time.Duration // 窗口时间大小
  16. }
  17.  
  18. func (*ViolationStrategyError) Error() string {
  19.      return fmt.Sprintf(“violation strategy that limit = %d and window = %d”, e.Limit, e.Window)
  20. }
  21.  
  22. // SlidingLogLimiterStrategy 滑动日志限流器的策略
  23. type SlidingLogLimiterStrategy struct {
  24.      limit int // 窗口请求上限
  25.      window int64 // 窗口时间大小
  26.      smallWindows int64 // 小窗口数量
  27. }
  28.  
  29. func NewSlidingLogLimiterStrategy(limit int, window time.Duration) *SlidingLogLimiterStrategy {
  30.      return &SlidingLogLimiterStrategy{
  31.          limit: limit,
  32.          window: int64(window),
  33.      }
  34. }
  35.  
  36. // SlidingLogLimiter 滑动日志限流器
  37. type SlidingLogLimiter struct {
  38.      strategies []*SlidingLogLimiterStrategy // 滑动日志限流器策略列表
  39.      smallWindow int64 // 小窗口时间大小
  40.      client *redis.Client // Redis客户端
  41.      script *redis.Script // TryAcquire脚本
  42. }
  43.  
  44. func NewSlidingLogLimiter(client *redis.Client, smallWindow time.Duration, strategies …*SlidingLogLimiterStrategy) (
  45.      *SlidingLogLimiter, error) {
  46.      // 复制策略避免被修改
  47.      strategies = append(make([]*SlidingLogLimiterStrategy, 0, len(strategies)), strategies…)
  48.  
  49.      // 不能不设置策略
  50.      if len(strategies) == 0 {
  51.          return nil, errors.New(“must be set strategies”)
  52.      }
  53.  
  54.      // redis过期时间精度最大到毫秒,因此窗口必须能被毫秒整除
  55.      if smallWindow%time.Millisecond != 0 {
  56.          return nil, errors.New(“the window uint must not be less than millisecond”)
  57.      }
  58.      smallWindow = smallWindow / time.Millisecond
  59.      for _, strategy := range strategies {
  60.          if strategy.window%int64(time.Millisecond) != 0 {
  61.              return nil, errors.New(“the window uint must not be less than millisecond”)
  62.          }
  63.          strategy.window = strategy.window / int64(time.Millisecond)
  64.      }
  65.  
  66.      // 排序策略,窗口时间大的排前面,相同窗口上限大的排前面
  67.      sort.Slice(strategies, func(i, j int) bool {
  68.          a, b := strategies[i], strategies[j]
  69.          if a.window == b.window {
  70.              return a.limit > b.limit
  71.          }
  72.          return a.window > b.window
  73.      })
  74.  
  75.      for i, strategy := range strategies {
  76.          // 随着窗口时间变小,窗口上限也应该变小
  77.          if i > 0 {
  78.              if strategy.limit >= strategies[i1].limit {
  79.              return nil, errors.New(“the smaller window should be the smaller limit”)
  80.              }
  81.          }
  82.          // 窗口时间必须能够被小窗口时间整除
  83.          if strategy.window%int64(smallWindow) != 0 {
  84.              return nil, errors.New(“window cannot be split by integers”)
  85.          }
  86.      strategy.smallWindows = strategy.window / int64(smallWindow)
  87.      }
  88.  
  89.      return &SlidingLogLimiter{
  90.          strategies: strategies,
  91.          smallWindow: int64(smallWindow),
  92.          client: client,
  93.          script: redis.NewScript(slidingLogLimiterTryAcquireRedisScriptHashImpl),
  94.      }, nil
  95. }
  96.  
  97. func (*SlidingLogLimiter) TryAcquire(ctx context.Context, resource string) error {
  98.      // 获取当前小窗口值
  99.      currentSmallWindow := time.Now().UnixMilli() / l.smallWindow * l.smallWindow
  100.      args := make([]interface{}, len(l.strategies)*2+2)
  101.      args[0] = currentSmallWindow
  102.      args[1] = l.strategies[0].window
  103.      // 获取每个策略的起始小窗口值
  104.      for i, strategy := range l.strategies {
  105.          args[i*2+2] = currentSmallWindow  l.smallWindow*(strategy.smallWindows1)
  106.          args[i*2+3] = strategy.limit
  107.      }
  108.  
  109.      index, err := l.script.Run(
  110.          ctx, l.client, []string{resource}, args…).Int()
  111.      if err != nil {
  112.          return err
  113.      }
  114.      // 若到达窗口请求上限,请求失败
  115.      if index != 1 {
  116.          return &ViolationStrategyError{
  117.              Limit: l.strategies[index].limit,
  118.              Window: time.Duration(l.strategies[index].window),
  119.          }
  120.      }
  121.      return nil
  122. }

总结

由于Redis拥有丰富而且高性能的数据类型,因此使用Redis实现限流算法并不困难,但是每个算法都需要编写Lua脚本,所以如果不熟悉Lua可能会踩一些坑。

需要完整代码和测试代码可以查看:github.com/jiaxwu/limiter/tree/main/redis

以上就是Go+Redis实现常见限流算法的示例代码的详细内容,更多关于Go Redis限流算法的资料请关注我们其它相关文章!

标签

发表评论