分佈式限流:基於 Redis 的有序集合

本文作者是來自國外一家名爲 ClassDojo 的科技公司,其分享了在企業構建推送通知服務的限流器實踐。該服務要求具備以下標準的限流功能:

我們查看了可用的限流器選擇,但在 NPM(node.js 包管理,他們的服務應該是基於 node.js 開發)未找到符合上述要求的包。所以決定自己寫,你可以在這裏下載。

方案一:token buckets(令牌桶)

使用滾動窗口限流的經典算法是令牌桶 (或漏桶算法)。下面是它的工作原理:

這是一種非常聰明的算法,只佔用很少的空間 (每個用戶只佔用一個整數計數器),但是這種直接的實現有一個很大的缺陷——一些進程需要不斷地填充桶。由於有數百萬用戶,而且每次填充操作都需要寫操作,這對我們的 Redis 實例來說是一個不可持續的負載。這裏有一個更高級的方法:

更好方法:sorted sets(有序集合)

幸運的是,Redis 有另一個數據結構,我們可以用來防止這些競爭條件 - 有序集合。這是我們想出的算法:

然而,這種方法有一個瑕疵——我們不受影響,但可能不適合他人的要求。在我們的算法中,你可以看到一個操作是否被阻塞是在所有 Redis 操作完成後決定的。這意味着被阻止的操作仍然算作操作。所以,如果用戶持續超過速率限制,他們的任何操作都將不被允許 (在前幾次之後),而不是允許偶爾的操作通過。

模塊

我們在 npm 上開源了我們的限制器,作爲 rolling-rate-limiter 模塊。它可以使用 Redis 後端,或者,如果你不需要在多進程中運行你的限流器,在內存中操作 (使用一個簡單的數組而不是排序集)。這裏有一個例子:

/*  Setup:  */

  var RateLimiter = require("rolling-rate-limiter");
  var Redis = require("redis");
  var client = Redis.createClient(config);

  var limiter = RateLimiter({
    redis: client,
    namespace: "UserLoginLimiter"
    interval: 1000
    maxInInterval: 10
    minDifference: 100
  });

  /*  Action:  */

  function attemptAction(userId, cb) {
    limiter(userId, function(err, timeLeft) {
      if (err) {

        // redis failed or similar.

      } else if (timeLeft > 0) {

        // 超出限制, 操作被阻止
        // timeLeft是允許進行下一個操作之前的毫秒數
        // 注意,這可以被當作一個布爾值,因爲0代表false

      } else {

        // 未超過限額,允許操作

      }
    });
  }

你也可以很容易地使用它與中間件結合對請求限速,如下所示:

var limiter = RateLimiter({
    redis: redisClient,
    namespace: "requestRateLimiter",
    interval: 60000,
    maxInInterval: 100,
    minDifference: 100
  });

  app.use(function(req, res, next) {

    // "req.ipAddress" 可以用任何唯一的用戶標識符替換
    limiter(req.ipAddress, function(err, timeLeft) {
      if (err) {
        return res.status(500).send();
      } else if (timeLeft) {
        return res.status(429).send("You must wait " + timeLeft + " ms before you can make requests.");
      } else {
        return next();
      }
    });

  });

你可以在 Github 上查看源代碼。我們希望這將有助於您編寫 Node.js 應用程序!

總結

本文介紹了一種基於 Redis 有序集合實現的分佈式限流器,該方法特別適合軟件通知服務限流。當然每種算法都不是放之四海而皆準的,需要根據自己的場景來選擇或者改造。雖然文章使用的是 node.js 實現,讀者也可以嘗試用 Go 來實踐下。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3mF7k4-tinW_DHq26G-wNA