下面我用**「概念 → 三种锁实现 → 完整示例 → 使用建议」,给你一份真正可落地的 PHP 互斥锁线程安全示例合集**👇
(覆盖:文件锁 / Redis 锁 / 数据库锁)
⚠️ 说明:
PHP 本身是多进程而非多线程,这里的“线程安全”指的是并发请求下的临界区安全。
一、为什么 PHP 需要互斥锁?
没加锁会发生什么?
$balance = file_get_contents('money.txt');
$balance -= 100;
file_put_contents('money.txt', $balance);
❌ 并发 2 个请求 → 余额只扣一次
二、方案一:文件锁(flock)【最简单】
适用场景
- 单机
- 小并发
- CLI / cron
示例:文件写入互斥
$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 分布式锁(推荐 ⭐)
适用场景
- Web 高并发
- 多服务器
- 秒杀 / 抢购
原理(一定要懂)
SET key value NX EX ttl
- NX:不存在才设置
- EX:过期时间(防死锁)
- value:唯一标识(防误删)
示例:安全版 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 的“线程安全”不是靠语言,而是靠“正确地使用互斥锁”
发表回复