前段時間,我使用了 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
本文地址:shanyue.tech/post/rate-l…緩存
漏桶算法表示水滴(請求)先進入到漏桶裏,漏桶(bucket)以必定的速度出水,當漏桶中水滿時,沒法再加水。app
用 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的 INCR
與 EXPIRE
的原子性問題,容易形成 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
由圖先看一看令牌桶與漏桶的不一樣
如下使用 redis 實現令牌桶
TODO