PHP令牌桶算法能实现吗

wen PHP项目 2

本文目录导读:

PHP令牌桶算法能实现吗

  1. 简单内存实现(单进程/单机)
  2. Redis 分布式实现(多机/集群)
  3. 高性能版本(带毫秒级精度)
  4. 框架集成示例(Laravel)
  5. 使用现成库
  6. 关键要点

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()) {
    // 请求通过
}

关键要点

  1. 并发安全:生产环境建议使用 Redis + Lua 脚本,保证原子性
  2. 精度控制:根据需求选择秒级或毫秒级精度
  3. 容量管理:合理设置桶容量和生成速率
  4. 内存考虑:内存实现适合单机,分布式必须用 Redis
  5. 性能优化:使用 Lua 脚本减少网络往返

令牌桶算法的优势在于允许一定程度的突发流量(桶容量),同时又限制了平均速率,非常适合 API 限流场景。

抱歉,评论功能暂时关闭!