Redis 在 PHP 生态中早已不只是缓存工具。凭借原子操作、Lua 脚本、过期机制和发布订阅等特性,它可以承担分布式锁、延时队列和限流器这类基础设施角色。本文从实战角度出发,结合 PHP 代码示例,讲解这三种高级用法的实现思路与关键细节。
一、分布式锁:从 SET NX 到 Redlock
1.1 为什么需要分布式锁
在单机环境中,PHP 的 flock() 或文件锁足以应对并发写入。但在多台 PHP-FPM 服务器同时处理同一业务时,文件锁失效,必须依赖外部协调服务。Redis 的 SET key value NX PX milliseconds 命令天然适合作为锁原语:NX 保证互斥,PX 保证锁自动释放,避免死锁。
1.2 基础实现
class RedisLock
{
private $redis;
private $prefix = 'lock:';
public function __construct(\Redis $redis)
{
$this->redis = $redis;
}
public function acquire(string $key, string $token, int $ttl = 10000): bool
{
$result = $this->redis->set(
$this->prefix . $key,
$token,
['nx', 'px' => $ttl]
);
return $result === true;
}
public function release(string $key, string $token): bool
{
// Lua 脚本保证“判断 + 删除”的原子性
$script = <<<'LUA'
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
return (bool) $this->redis->eval(
$script,
[$this->prefix . $key, $token],
1
);
}
}
关键点有两个:value 必须使用唯一 token(如 uniqid('', true)),防止误删他人持有的锁;释放锁必须用 Lua 脚本,保证 GET 和 DEL 之间不被其他客户端插入操作。
1.3 锁续期与 Redlock 的取舍
如果业务执行时间可能超过 TTL,需要启动一个“看门狗”协程或定时任务定期续期。PHP 中可以用 Swoole\Timer 或简单的 pcntl_alarm 实现。对于跨多个 Redis 主节点的高可用场景,Redis 官方提出的 Redlock 算法要求在多数节点上成功获取锁才算成功。不过 Redlock 在业界存在争议(Martin Kleppmann 曾质疑其安全性),若业务对一致性要求极高,建议使用 etcd 或 ZooKeeper。对于大多数 PHP 业务,单实例 Redis 锁配合合理 TTL 已足够。
二、延时队列:用 ZSET 实现精确调度
2.1 业务场景
订单 30 分钟未支付自动取消、用户注册 24 小时后发送召回短信——这类需求不适合用 sleep 阻塞进程,也不适合用 RabbitMQ 的延迟插件(增加运维复杂度)。Redis 的 ZSET(有序集合)以时间戳为 score,天然支持按时间排序取出到期任务。
2.2 生产者:投递延时任务
class DelayQueue
{
private $redis;
private $key = 'delay:queue';
public function __construct(\Redis $redis)
{
$this->redis = $redis;
}
public function push(string $job, int $delaySeconds): void
{
$score = microtime(true) + $delaySeconds;
$this->redis->zAdd($this->key, $score, $job);
}
}
// 投递一个 30 分钟后执行的订单取消任务
$queue->push(json_encode([
'type' => 'cancel_order',
'order_id' => 1024,
]), 1800);
2.3 消费者:轮询到期任务
public function consume(int $batch = 10): array
{
$now = microtime(true);
// 取出 score <= 当前时间的任务
$jobs = $this->redis->zRangeByScore(
$this->key,
'-inf',
(string) $now,
['limit' => [0, $batch]]
);
$result = [];
foreach ($jobs as $job) {
// ZREM 返回 1 表示抢到该任务,避免多消费者重复处理
if ($this->redis->zRem($this->key, $job) === 1) {
$result[] = $job;
}
}
return $result;
}
消费者可以用 while(true) 循环加 usleep(500000) 轮询,也可以借助 zRangeByScore 的阻塞替代方案——用 Redis 的 BLPOP 配合一个“最近到期时间”通知键,减少空轮询。生产环境中建议用 Supervisor 管理多个消费者进程,并加入失败重试逻辑(如处理失败后重新 zAdd 并增加重试计数)。
三、限流器:滑动窗口与令牌桶
3.1 固定窗口的缺陷
最简单的限流是 INCR + EXPIRE,比如限制每分钟 100 次请求。但固定窗口存在临界问题:在第 59 秒和第 61 秒各发 100 次请求,虽然两个窗口内都没超限,但实际 2 秒内通过了 200 次请求。滑动窗口能解决这个问题。
3.2 滑动窗口限流(ZSET 实现)
class SlidingWindowLimiter
{
private $redis;
public function __construct(\Redis $redis)
{
$this->redis = $redis;
}
public function allow(string $key, int $limit, int $window): bool
{
$now = microtime(true);
$zkey = 'limit:' . $key;
$pipe = $this->redis->multi(\Redis::PIPELINE);
// 移除窗口外的旧请求
$pipe->zRemRangeByScore($zkey, '-inf', (string) ($now - $window));
// 统计当前窗口内请求数
$pipe->zCard($zkey);
// 记录本次请求
$pipe->zAdd($zkey, $now, $now . ':' . mt_rand());
// 设置过期,避免冷 key 占用内存
$pipe->expire($zkey, $window + 1);
$results = $pipe->exec();
$count = $results[1];
if ($count >= $limit) {
// 超限,回滚刚才的 zAdd
$this->redis->zRemRangeByScore($zkey, (string) $now, (string) $now);
return false;
}
return true;
}
}
这里用 Pipeline 减少网络往返,但严格来说“统计 + 添加”不是原子的。若要求绝对精确,应改用 Lua 脚本一次性完成。Lua 版本只需把上述逻辑写进 eval() 即可,性能也更好。
3.3 令牌桶的替代方案
滑动窗口限制的是“窗口内请求数”,而令牌桶允许突发流量。Redis 官方在 redis-cell 模块中提供了 CLTHROTTLE 命令,一条命令实现令牌桶。若无法安装模块,可以用 Lua 模拟:维护 tokens 和 last_refill 两个字段,每次请求按时间差补充令牌。对于 PHP 应用,滑动窗口已能覆盖 90% 的限流场景,令牌桶更适合网关层。
四、生产环境注意事项
连接管理:使用 pconnect 长连接可减少 TCP 开销,但在 PHP-FPM 下需注意连接数上限。推荐用 RedisArray 或 RedisCluster 做分片。
序列化:igbinary 比 PHP 默认序列化更快更省内存,安装扩展后设置 $redis->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_IGBINARY)。
监控:用 INFO 命令关注 blocked_clients、used_memory 和 instantaneous_ops_per_sec,限流器和延时队列容易因 key 过多导致内存膨胀。
降级:Redis 不可用时,分布式锁应快速失败而非阻塞,限流器可切换为本地内存计数,延时队列需将任务落库兜底。
结语
分布式锁、延时队列和限流器是 PHP 后端进阶的必修课。Redis 用简洁的数据结构解决了这些问题,但“能用”和“用好”之间隔着原子性、续期、临界问题和降级策略等细节。建议在测试环境用 redis-benchmark 压测,并用 Lua 脚本替代多命令组合,让每一步操作都具备原子语义。掌握这些模式后,你会发现 Redis 不只是缓存,更是轻量级协调服务的瑞士军刀。
未经允许不得转载:任鹏个人博客 » Redis 在 PHP 中的高级用法:分布式锁、延时队列与限流器


朋友圈点赞图在线生成源码