前段时间,我使用了 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