本文目录导读:

在 PHP 项目中,选择公平锁还是非公平锁,核心取决于你的业务场景以及对性能和公平性的权衡,由于 PHP 通常不是原生支持多线程(CLI 下的 pthreads 或 parallel 扩展除外),这里的“锁”更多指分布式锁(如 Redis、MySQL、etcd 实现)或进程间锁(如文件锁、信号量)。
以下是详细的业务适配分析和选择指南:
核心概念回顾(仅针对分布式/进程间锁)
- 公平锁:先请求锁的线程/进程,先获得锁(按请求顺序排队)。
- 实现方式:通常用 Redis 有序集合(ZSet)或消息队列 + 锁,每个请求加锁时先排队(按时间戳或自增 ID),锁释放后让队首进程获取。
- 优点:避免“饥饿”现象(某个进程永远拿不到锁)。
- 缺点:性能较低,需要额外的排序和队列维护开销。
- 非公平锁:所有请求随机或按抢占方式争夺锁。
- 实现方式:直接 SETNX(Redis)或
flock+ 自旋。 - 优点:性能高,吞吐量大(无需排队)。
- 缺点:可能造成“线程/进程饥饿”,高并发下锁被频繁抢占,某些进程可能长时间等待。
- 实现方式:直接 SETNX(Redis)或
业务适配选择决策树
| 场景特征 | 推荐锁类型 | 原因 |
|---|---|---|
| 严格先来后到 (如:秒杀排队号、订单号生成、排队叫号系统) | 公平锁 | 必须保证请求顺序,否则用户体验灾难(后请求先处理)。 |
| 高吞吐、允许偶尔插队 (如:缓存刷新、日志同步、非关键数据清理) | 非公平锁 | 性能优先,少量进程多等几毫秒无感知。 |
| 锁持有时间很长 (如:生成大报表、数据迁移) | 公平锁 | 防止一个长任务被无限打断,导致其他任务一直失败重试(即饥饿)。 |
| 锁持有时间极短 (如:原子递增计数器、简单写入) | 非公平锁 | 抢占成本低,排队反而浪费时间。 |
| 资源本身具有排他性 (如:唯一订单号、支付防重) | 两者皆可 | 如果业务不要求顺序,非公平更高效;如果要求顺序(如退款处理),选公平。 |
| 重试成本高 (如:每次获取锁失败需要复杂回滚) | 公平锁 | 减少无意义的自旋重试,按顺序稳定获取。 |
| 请求量极大且锁竞争激烈 (>5000 QPS) | 非公平锁 | 公平锁在抢占锁时,队列排序本身会成为瓶颈(需要 O(log N) 操作每次)。 |
PHP 中的具体实现方案
Redis 实现(最常用)
-
非公平锁:
// 标准 SETNX + EXPIRE + 随机值 $lockKey = 'resource_lock'; $lockValue = uniqid('', true); $ttl = 10; // 秒 if ($redis->set($lockKey, $lockValue, ['NX', 'EX' => $ttl])) { // 获得锁,执行业务逻辑 // ... // 释放锁(需要 Lua 脚本保证原子性) $script = 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end'; $redis->eval($script, [$lockKey, $lockValue], 1); } else { // 获取锁失败,sleep 后重试(非公平的自旋) usleep(500 * 1000); // 500ms } -
公平锁(基于 Redis 有序集合 + Lua):
// 思路:每个请求插入 ZSet(score = 毫秒时间戳 + 请求ID), // 只有 score 最小的请求(队首)才能加锁。 // 下面简化代码,实际生产中需要 Lua 保证原子性队列操作。 $queueKey = 'lock_queue'; $lockKey = 'resource_lock'; $myScore = microtime(true) * 10000; // 高精度分数 // 1. 入队并将当前请求加入队列 $redis->zadd($queueKey, $myScore, session_id()); // session_id 仅示例 // 2. 加锁尝试(ACID 检查) $script = ' local queue = KEYS[1] local lock = KEYS[2] local myId = ARGV[1] -- 获取当前队列的最小score元素 local min = redis.call("zrange", queue, 0, 0, "WITHSCORES") if (#min == 0) then return 0 end -- 队列空 if (min[1] == myId) then -- 我是队首,尝试加锁 local result = redis.call("set", lock, myId, "NX", "EX", 10) if (result) then return 1 else return 0 -- 锁被占(理论上不会发生,但考虑并发) end else return 0 -- 我不是队首,不获得锁 end '; $result = $redis->eval($script, [$queueKey, $lockKey, session_id()], 2); if ($result === 1) { // 获得锁,业务处理 // ... // 释放锁时:从队列删除自己 + 删除锁 $redis->zrem($queueKey, session_id()); $redis->del($lockKey); } else { // 没获得锁,等待并重试(轮询队列) // 注意:避免太频繁调用ZRANGE造成性能问题,建议加随机等待 usleep(200 * 1000); }注:上述公平锁实现极简,生产环境中建议直接使用 Redlock 的变体或成熟的库(如
mutex)。
MySQL 实现
- 非公平锁:
SELECT ... FOR UPDATE(行锁) - 公平锁:MySQL 原生不支持排队的行锁,需要自己维护一个等待队列表 + 事务顺序控制(复杂且低效)。
文件锁(仅单进程/单机)
- 非公平:
flock($fp, LOCK_EX)(默认就是非公平,实际效果是随机的) - 公平:PHP 原生
flock不支持公平锁,需要自己用sem_get+ 信号量模拟(但信号量本身也是非公平的)。
性能与公平性的真实权衡
| 维度 | 非公平锁 | 公平锁 |
|---|---|---|
| 平均获取延迟 | 低 (约0.1ms) | 高 (可能 5-50ms) |
| 最大等待时间 | 理论无限(可能永远获取不到) | 有界(按顺序,最坏 = 持有时间 × N) |
| 数据库/Redis 压力 | 低(一次 Set+NX) | 高(需要维护队列 ZADD/ZRANGE 或有序列表) |
| PHP 代码复杂度 | 低 | 高(需要处理队列原子性、过期清理、节点宕机) |
实战建议:混合使用
在大多数生产 PHP 项目中,非公平锁占 90% 以上,但遇到以下情况才切换公平锁:
- 需要“排他且有序”的操作:
- 生成递增发票号(必须按请求顺序分配)。
- 秒杀最终确认队列(防止后抢到的人先付款成功)。
- 长时间任务守护:
数据迁移脚本(比如一次迁移 10 万条),如果非公平,可能每次刚获得锁就被其他脚本抢走(产生“饥饿”),需要公平锁保证至少完成一轮。
- 第三方 API 频率限流补偿:
多个 PHP 进程使用一个桶(Token bucket),如果非公平,可能导致某些进程长期拿不到令牌,需公平排队。
成熟库推荐
- Symfony Lock 组件:支持 Redis/MySQL/File 锁,默认非公平,但可以配置 Blocking锁(类似公平等待)。
- predis/predis:配合 Lua 可实现简单公平锁。
- hyperf/lock:为 Swoole 协程优化的锁,默认非公平,支持超时。
一句话决策
- 非公平锁 → 大多数 PHP 场景(API 限流、缓存防击穿、写日志、定时任务防重复)。
- 公平锁 → 用户感知到“等待顺序”的场景(排队、下单顺序、资源分配严格的金融操作)。
最后建议:默认选择非公平锁,只在明确需要保证“按请求顺序处理”时才使用公平锁,在 PHP 的分布式环境中,实现一个高性能的公平锁远比非公平复杂,且容易引入新问题(队列堆积、死锁)。
版权声明: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直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播