本文目录导读:

PHP 完全可以实现令牌桶算法,而且实现方式很灵活,下面是几种不同场景的实现方式:
简单内存实现(单进程/单机)
<?php
class TokenBucket {
private $capacity; // 桶容量
private $tokens; // 当前令牌数
private $rate; // 令牌生成速率(个/秒)
private $lastRefill; // 上次补充时间
public function __construct($capacity, $rate) {
$this->capacity = $capacity;
$this->tokens = $capacity; // 初始满桶
$this->rate = $rate;
$this->lastRefill = time();
}
public function consume($tokens = 1) {
$this->refill();
if ($this->tokens >= $tokens) {
$this->tokens -= $tokens;
return true;
}
return false;
}
private function refill() {
$now = time();
$interval = $now - $this->lastRefill;
if ($interval > 0) {
$newTokens = $interval * $this->rate;
$this->tokens = min($this->capacity, $this->tokens + $newTokens);
$this->lastRefill = $now;
}
}
// 获取当前令牌数
public function getTokens() {
$this->refill();
return $this->tokens;
}
}
// 使用示例
$bucket = new TokenBucket(10, 2); // 容量10,每秒生成2个令牌
if ($bucket->consume()) {
echo "请求通过\n";
} else {
echo "请求被限流\n";
}
?>
Redis 分布式实现(多机/集群)
<?php
class RedisTokenBucket {
private $redis;
private $key;
private $capacity;
private $rate;
public function __construct($redis, $key, $capacity, $rate) {
$this->redis = $redis;
$this->key = $key;
$this->capacity = $capacity;
$this->rate = $rate;
}
public function consume($tokens = 1) {
$luaScript = <<<LUA
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local tokens = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
-- 获取当前令牌数
local currentTokens = redis.call('get', key)
if currentTokens == false then
currentTokens = capacity
else
currentTokens = tonumber(currentTokens)
end
-- 获取上次补充时间
local lastRefill = redis.call('get', key .. '_time')
if lastRefill == false then
lastRefill = now
else
lastRefill = tonumber(lastRefill)
end
-- 计算需要补充的令牌数
local elapsed = now - lastRefill
if elapsed > 0 then
local newTokens = elapsed * rate
currentTokens = math.min(capacity, currentTokens + newTokens)
redis.call('set', key .. '_time', now)
end
-- 尝试消耗令牌
if currentTokens >= tokens then
currentTokens = currentTokens - tokens
redis.call('set', key, currentTokens)
redis.call('expire', key, 3600)
return 1
else
redis.call('set', key, currentTokens)
redis.call('expire', key, 3600)
return 0
end
LUA;
$result = $this->redis->eval(
$luaScript,
[$key, $this->capacity, $this->rate, $tokens, time()],
1
);
return $result === 1;
}
}
// 使用示例
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$bucket = new RedisTokenBucket($redis, 'api_limit', 100, 10);
if ($bucket->consume()) {
echo "请求通过\n";
} else {
echo "请求被限流\n";
}
?>
高性能版本(带毫秒级精度)
<?php
class PreciseTokenBucket {
private $capacity;
private $tokens;
private $rate;
private $lastRefillMicro;
public function __construct($capacity, $rate) {
$this->capacity = $capacity;
$this->tokens = $capacity;
$this->rate = $rate;
$this->lastRefillMicro = microtime(true);
}
public function consume($tokens = 1) {
$this->refill();
if ($this->tokens >= $tokens) {
$this->tokens -= $tokens;
return true;
}
return false;
}
private function refill() {
$now = microtime(true);
$interval = $now - $this->lastRefillMicro;
if ($interval > 0.001) { // 最小补充间隔1ms
$newTokens = $interval * $this->rate;
$this->tokens = min($this->capacity, $this->tokens + $newTokens);
$this->lastRefillMicro = $now;
}
}
}
// 使用示例
$bucket = new PreciseTokenBucket(100, 20); // 容量100,每秒20个令牌
for ($i = 0; $i < 10; $i++) {
usleep(50000); // 50ms
if ($bucket->consume()) {
echo "第{$i}次请求通过\n";
} else {
echo "第{$i}次请求被拒绝\n";
}
}
?>
框架集成示例(Laravel)
<?php
// 在 Laravel 中创建一个中间件
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
use Illuminate\Http\Request;
class RateLimiter
{
public function handle(Request $request, Closure $next, $capacity = 100, $rate = 10)
{
$key = 'rate_limit:' . $request->ip();
$luaScript = <<<LUA
-- Lua脚本实现令牌桶
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
if not redis.call('exists', key) then
redis.call('hset', key, 'tokens', capacity, 'time', now)
end
local data = redis.call('hgetall', key)
local tokens = tonumber(data[2])
local lastTime = tonumber(data[4])
local elapsed = now - lastTime
tokens = math.min(capacity, tokens + elapsed * rate)
if tokens >= 1 then
tokens = tokens - 1
redis.call('hset', key, 'tokens', tokens, 'time', now)
redis.call('expire', key, 60)
return 1
else
redis.call('hset', key, 'tokens', tokens, 'time', now)
redis.call('expire', key, 60)
return 0
end
LUA;
$result = Redis::eval(
$luaScript,
[$key, $capacity, $rate, time()],
1
);
if (!$result) {
return response()->json(['message' => 'Too Many Requests'], 429);
}
return $next($request);
}
}
?>
使用现成库
也可以使用现成的 PHP 库:
// 通过 Composer 安装
// composer require predis/predis
// 使用 Predis + Redis 实现限流
$client = new Predis\Client();
$bucket = $client->createRateLimiter('api', 100, 10);
if ($bucket->acquire()) {
// 请求通过
}
关键要点
- 并发安全:生产环境建议使用 Redis + Lua 脚本,保证原子性
- 精度控制:根据需求选择秒级或毫秒级精度
- 容量管理:合理设置桶容量和生成速率
- 内存考虑:内存实现适合单机,分布式必须用 Redis
- 性能优化:使用 Lua 脚本减少网络往返
令牌桶算法的优势在于允许一定程度的突发流量(桶容量),同时又限制了平均速率,非常适合 API 限流场景。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播