最近想到一个关于限流的问题, 比如一个 api 限制每秒最多请求 100 次,那么我们在本地就需要做这样一个限制机制,来保证任意一个 1 秒的时间段内,请求次数都小于 100 次。
我们设时间段为 T (这里是 1 秒),请求次数为 R (这里是 100 次),然后取正整数 n (n > 1),表示将时间段平均分成 n 份。定义 t = T / n
为每份的时长。定义 r(t(i)) = R - sum(r‘(t(i-n))...r’(t(i-1)))
, 表示任意一份时间片中的最大请求次数,等于 R 减去过去 n 份时间片的请求次数总和。抱歉我还没有开始学 Latex,没法准确地写出公式。总之按照这种方法可以满足我们的需求,即任意一个 T 的时间段内,请求次数都小于 R 次。
下面是一个简单的实现:
defmodule Limit do
use GenServer
@n 30
@_R 100
# ms
@_T 1000
def req() do
GenServer.call(__MODULE__, :req)
end
def start() do
if GenServer.whereis(__MODULE__) do
GenServer.stop(__MODULE__)
end
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
def init(_) do
:timer.send_interval(div(@_T, @n), :update)
{:ok,
%{
queue: :queue.from_list(List.duplicate(0, @n)),
r_max: @_R,
r: 0
}}
end
def handle_call(:req, _from, state = %{r: r, r_max: r_max}) do
r = r + 1
if r <= r_max do
{:reply, :ok, %{state | r: r}}
else
{:reply, {:error, :limit}, state}
end
end
def handle_info(:update, state = %{r: r, queue: q}) do
## debug
# :queue.to_list(q) |> IO.inspect()
{_, q} = :queue.out(q)
q = :queue.in(r, q)
s = :queue.fold(fn x, acc -> x + acc end, 0, q)
r_max = @_R - s
{:noreply, %{state | r: 0, r_max: r_max, queue: q}}
end
end
测试用例:
defmodule LimitTest do
use ExUnit.Case
test "100 request at same time should all return ok" do
Limit.start()
r =
for _ <- 1..100 do
Limit.req()
end
|> Enum.all?(fn x -> x == :ok end)
assert r
end
test "101 request at same time should return 100 ok and 1 error at last req" do
Limit.start()
r =
for _ <- 1..100 do
Limit.req()
end
|> Enum.all?(fn x -> x == :ok end)
assert r
assert Limit.req() == {:error, :limit}
end
test "request capacity should re-fill after 1 second (500 ms more to avoid race)" do
Limit.start()
for _ <- 1..100 do
Limit.req()
end
:timer.sleep(1500)
assert Limit.req() == :ok
end
end
这种基于历史状态的算法在很多领域都用被应用,例如比特币网络中会根据过去一段时间的出块速度来调整当前的工作量证明难度,以使得出块时间保持在 10 分钟左右。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。