PHP项目公平锁非公平锁如何选择业务适配

wen PHP项目 23

本文目录导读:

PHP项目公平锁非公平锁如何选择业务适配

  1. 核心概念回顾(仅针对分布式/进程间锁)
  2. 业务适配选择决策树
  3. PHP 中的具体实现方案
  4. 性能与公平性的真实权衡
  5. 实战建议:混合使用
  6. 成熟库推荐
  7. 一句话决策

在 PHP 项目中,选择公平锁还是非公平锁,核心取决于你的业务场景以及对性能公平性的权衡,由于 PHP 通常不是原生支持多线程(CLI 下的 pthreadsparallel 扩展除外),这里的“锁”更多指分布式锁(如 Redis、MySQL、etcd 实现)或进程间锁(如文件锁、信号量)。

以下是详细的业务适配分析和选择指南:


核心概念回顾(仅针对分布式/进程间锁)

  • 公平锁先请求锁的线程/进程,先获得锁(按请求顺序排队)。
    • 实现方式:通常用 Redis 有序集合(ZSet)或消息队列 + 锁,每个请求加锁时先排队(按时间戳或自增 ID),锁释放后让队首进程获取。
    • 优点:避免“饥饿”现象(某个进程永远拿不到锁)。
    • 缺点:性能较低,需要额外的排序和队列维护开销。
  • 非公平锁所有请求随机或按抢占方式争夺锁
    • 实现方式:直接 SETNX(Redis)或 flock + 自旋。
    • 优点:性能高,吞吐量大(无需排队)。
    • 缺点:可能造成“线程/进程饥饿”,高并发下锁被频繁抢占,某些进程可能长时间等待。

业务适配选择决策树

场景特征 推荐锁类型 原因
严格先来后到 (如:秒杀排队号、订单号生成、排队叫号系统) 公平锁 必须保证请求顺序,否则用户体验灾难(后请求先处理)。
高吞吐、允许偶尔插队 (如:缓存刷新、日志同步、非关键数据清理) 非公平锁 性能优先,少量进程多等几毫秒无感知。
锁持有时间很长 (如:生成大报表、数据迁移) 公平锁 防止一个长任务被无限打断,导致其他任务一直失败重试(即饥饿)。
锁持有时间极短 (如:原子递增计数器、简单写入) 非公平锁 抢占成本低,排队反而浪费时间。
资源本身具有排他性 (如:唯一订单号、支付防重) 两者皆可 如果业务不要求顺序,非公平更高效;如果要求顺序(如退款处理),选公平。
重试成本高 (如:每次获取锁失败需要复杂回滚) 公平锁 减少无意义的自旋重试,按顺序稳定获取。
请求量极大且锁竞争激烈 (>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% 以上,但遇到以下情况才切换公平锁:

  1. 需要“排他且有序”的操作
    • 生成递增发票号(必须按请求顺序分配)。
    • 秒杀最终确认队列(防止后抢到的人先付款成功)。
  2. 长时间任务守护

    数据迁移脚本(比如一次迁移 10 万条),如果非公平,可能每次刚获得锁就被其他脚本抢走(产生“饥饿”),需要公平锁保证至少完成一轮。

  3. 第三方 API 频率限流补偿

    多个 PHP 进程使用一个桶(Token bucket),如果非公平,可能导致某些进程长期拿不到令牌,需公平排队。


成熟库推荐

  • Symfony Lock 组件:支持 Redis/MySQL/File 锁,默认非公平,但可以配置 Blocking锁(类似公平等待)。
  • predis/predis:配合 Lua 可实现简单公平锁。
  • hyperf/lock:为 Swoole 协程优化的锁,默认非公平,支持超时。

一句话决策

  • 非公平锁大多数 PHP 场景(API 限流、缓存防击穿、写日志、定时任务防重复)。
  • 公平锁用户感知到“等待顺序”的场景(排队、下单顺序、资源分配严格的金融操作)。

最后建议:默认选择非公平锁,只在明确需要保证“按请求顺序处理”时才使用公平锁,在 PHP 的分布式环境中,实现一个高性能的公平锁远比非公平复杂,且容易引入新问题(队列堆积、死锁)。

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