[登陸那些事] 郵件發送,限流,漏桶與令牌桶

前段時間,我使用了 jwt 來實現郵箱驗證碼的校驗與用戶認證與登陸,還特別寫了一篇文章做爲總結。javascript

在那篇文章中,提到了一個點,如何限速。java

在短信驗證碼和郵箱驗證碼,若是不限速,被惡意攻擊形成大量的 QPS,不只拖垮了服務,也會心疼如水的資費。鑑於君子固窮的原則,在個人郵箱服務里加上限速。redis

關於如何限速,有兩個比較出名的算法,漏桶算法與令牌桶算法,這裏對其簡單介紹一下,最後再實踐在我發郵件的API中算法

如下是發送郵件的 API,已限制爲一分鐘兩次,你能夠經過修改 email 進行試驗。你也能夠在個人站點直接試驗shell

curl 'https://graphql.xiange.tech/graphql' -H 'Content-Type: application/json' --data-binary '{"query":"mutation SEND($email: String!) {\n  sendEmailVerifyCode (email: $email)\n}","variables":{"email":"xxxxxx@qq.com"}}'
複製代碼

如下是我關於登陸實踐的系列文章json

  1. 【登陸那些事】實現 Material Design 的登陸樣式
  2. 【登陸那些事】使用 jwt 登陸與校驗驗證碼
  3. 【登陸那些事】郵件發送,限流,漏桶與令牌桶

本文地址:shanyue.tech/post/rate-l…緩存

Leaky Bucket (漏桶算法)

漏桶算法

漏桶算法表示水滴(請求)先進入到漏桶裏,漏桶(bucket)以必定的速度出水,當漏桶中水滿時,沒法再加水。app

  • 維護一個計數器做爲 bucket,計數器的上限爲 bucket 的大小
  • 計數器滿時拒絕請求
  • 每隔一段時間清空計數器

option 表明在 option.window 的窗口時間內最多能夠經過 option.max 次請求curl

如下是使用 redis 的計數器實現限流的僞代碼ide

const option = {
  max: 10,        // window 時間內限速10個請求
  window: 1000    // 1s
}

function access(req) {
  // 根據請求生成惟一標誌
  const key = identity(req)
  // 計數器自增
  const counter = redis.incr(key)
  if (counter === 1) {
    // 若是是當前時間窗口的第一個請求,設置過時時間
    redis.expire(key, window) 
  }
  if (counter > option.window) {
    return false
  }
  return true
}
複製代碼

這裏有 Redis 官方使用 INCR 實現限流的文檔 redis.io/commands/IN…

此時有一個不算問題的問題,就是它的時間窗口並非滑動窗口那樣在桶裏出去一個球,就能夠再進來一個球。而更像是一個固定時間窗口,從桶裏出去一羣球,再開始進球。正由於如此,它可能在固定窗口的後一半時間收到 max-1 次請求,又在下一個固定窗口內打來 max 次請求,此時在一個隨機的窗口時間內最多會有 2 * max - 1 次請求。

另外還有一個redis的 INCREXPIRE 的原子性問題,容易形成 Race Condition,能夠經過 SETNX 來解決

redis.set(key, 0, 'EX', option.window, 'NX')
複製代碼

另外也能夠經過一個 LUA 腳原本搞定,顯然仍是 SETNX 簡單些

local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],1)
end
複製代碼

爲了解決 2N 的問題,能夠由維護一個計數器,更改成維護一個隊列。代價是內存佔用空間太高,且更難解決 Race Condition

如下是使用 redis 的 set/get string 實現的限流

const option = {
  max: 10,        // window 時間內限速10個請求
  window: 1000    // 1s
}

function access(req) {
  // 根據請求生成惟一標誌
  const key = identity(req)
  const current = Date.now()
  // cache 視爲緩存對象
  // 篩選出當前時間窗口的請求個數,每一個請求標誌爲時間戳的格式
  // 爲了簡單這裏不作 json 的序列化和反序列化了...
  const timestamps = [current].concat(redis.get('timestamps')).filter(ts => ts + option.window > current)
  if (timestamps.length > option.max) {
    return false 
  }
  // 此時讀寫不一樣步,會有 Race Condition 問題
  redis.set('timestamps', timestamps, 'EX', option.window)
  return true
}
複製代碼

這裏再使用一個 LUA 腳本解決 Race Condition 的問題

TODO

Token Bucket (令牌桶算法)

令牌算法

由圖先看一看令牌桶與漏桶的不一樣

  1. 令牌桶初始狀態 bucket 是滿的,漏桶初始狀態 bucket 是空的
  2. 令牌桶在 bucket 空的時候拒絕新的請求,漏桶在 bucket 滿的時候拒絕新的請求
  3. 當一個請求來臨時,假設一個請求消耗一個token,令牌桶的 bucket 減小一個 token,漏桶增長一個 token

如下使用 redis 實現令牌桶

TODO

相關文章
相關標籤/搜索