菜鸟-创作你的创作

PHP使用互斥锁确保代码的线程安全的操作示例

下面我用**「概念 → 三种锁实现 → 完整示例 → 使用建议」,给你一份真正可落地的 PHP 互斥锁线程安全示例合集**👇
(覆盖:文件锁 / Redis 锁 / 数据库锁

⚠️ 说明:
PHP 本身是多进程而非多线程,这里的“线程安全”指的是并发请求下的临界区安全


一、为什么 PHP 需要互斥锁?

没加锁会发生什么?

$balance = file_get_contents('money.txt');
$balance -= 100;
file_put_contents('money.txt', $balance);

❌ 并发 2 个请求 → 余额只扣一次


二、方案一:文件锁(flock)【最简单】

适用场景


示例:文件写入互斥

$fp = fopen('lock.txt', 'c');

if (flock($fp, LOCK_EX)) {   // 获取排他锁
    $balance = (int) file_get_contents('money.txt');
    sleep(1); // 模拟耗时操作
    $balance -= 100;
    file_put_contents('money.txt', $balance);
    flock($fp, LOCK_UN);     // 释放锁
}

fclose($fp);

特点

优点缺点
简单只能单机
无依赖NFS 不可靠

三、方案二:Redis 分布式锁(推荐 ⭐)

适用场景


原理(一定要懂)

SET key value NX EX ttl


示例:安全版 Redis 锁

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$lockKey = 'lock:order:123';
$token   = uniqid();

$isLock = $redis->set($lockKey, $token, ['nx', 'ex' => 5]);

if ($isLock) {
    try {
        // 临界区代码
        processOrder();
    } finally {
        // Lua 脚本保证原子释放
        $lua = '
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end';
        $redis->eval($lua, [$lockKey, $token], 1);
    }
}


为什么不用 DEL key

❌ 锁过期 → 被别人拿到 → 你删了别人的锁
✔ 用 token 校验才能安全


四、方案三:MySQL 锁(数据库级)

方式 1:悲观锁(SELECT FOR UPDATE

$pdo->beginTransaction();

$stmt = $pdo->query("SELECT balance FROM account WHERE id=1 FOR UPDATE");
$balance = $stmt->fetchColumn();

sleep(1);
$pdo->exec("UPDATE account SET balance = balance - 100 WHERE id=1");

$pdo->commit();

📌 必须在事务中


方式 2:MySQL 命名锁(GET_LOCK)

$stmt = $pdo->query("SELECT GET_LOCK('order_123', 5)");
if ($stmt->fetchColumn()) {
    try {
        processOrder();
    } finally {
        $pdo->query("SELECT RELEASE_LOCK('order_123')");
    }
}


MySQL 锁对比

类型特点
FOR UPDATE行级锁
GET_LOCK进程级互斥

五、真实业务完整示例(防止订单重复支付)

Redis 锁 + 幂等

function payOrder($orderId) {
    global $redis;

    $lockKey = "pay:$orderId";
    $token = uniqid();

    if (! $redis->set($lockKey, $token, ['nx', 'ex' => 10])) {
        throw new Exception('订单正在处理中');
    }

    try {
        // 支付逻辑
        doPay();
        markPaid();
    } finally {
        $redis->eval(
            'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) end',
            [$lockKey, $token],
            1
        );
    }
}


六、三种方案该怎么选?

场景推荐
单机脚本flock
Web 并发Redis
强一致事务MySQL
分布式系统Redis

七、互斥锁使用黄金法则(必看)

✔ 锁粒度要小(精确到 ID)
✔ 必须设置超时
✔ 一定 try-finally
✔ 不要锁里写慢 IO
✔ 不要嵌套锁(死锁)


八、一句话总结

PHP 的“线程安全”不是靠语言,而是靠“正确地使用互斥锁”

退出移动版