PHP项目共享锁排他锁如何合理选用场景

wen PHP项目 27

本文目录导读:

PHP项目共享锁排他锁如何合理选用场景

  1. 核心区别回顾
  2. 场景选用指南
  3. 合理选用的决策流(场景决策树)
  4. 常见误区与最佳实践
  5. 性能对比(文件锁为例)
  6. 总结一句话

在 PHP 项目中,合理选用共享锁(读锁)和排他锁(写锁)的核心原则是根据并发访问中数据一致性与性能的平衡需求

简单记忆:读多写少用共享锁(提高并发),写多或写操作依赖读结果用排他锁(保证安全)。

下面从场景、代码示例、性能权衡三个维度详细说明。


核心区别回顾

锁类型 英文 别称 特点 能否并发读 能否并发写
共享锁 Shared Lock (S) 读锁 允许多个进程同时读取
排他锁 Exclusive Lock (X) 写锁 只允许一个进程独占

场景选用指南

场景 1:数据缓存重建(高频、典型)

背景:缓存过期后,多个请求同时回源数据库查询并写入缓存,如果不加锁,会导致“缓存击穿”或“雪崩”。

推荐:排他锁(写锁)

  • 原因:只允许一个进程去查数据库并写缓存,其他进程等待或直接使用旧缓存。
  • 反例:如果只用共享锁,多个进程会同时读到“缓存不存在”的状态,然后都去查询数据库,造成数据库压力暴增。
// 伪代码:使用文件锁实现
$lockFile = '/tmp/cache_lock_user_123.lock';
$fp = fopen($lockFile, 'w+');
flock($fp, LOCK_EX); // 排他锁:只允许一个进程进入
$data = getFromDatabase(); // 耗时操作
setToCache($key, $data, 3600);
flock($fp, LOCK_UN);
fclose($fp);

场景 2:高并发读取、低频率写入(如配置信息)

背景:一个秒杀活动的配置(如开始时间、限购数量),大部分时间被读取,偶尔由后台管理员修改。

推荐:共享锁(读锁)

  • 原因:允许数百个请求同时读取配置,互不阻塞,性能极高。
  • 注意:修改时必须先加排他锁,或改为只从主库/写库读取(如 MySQL 读写分离)。
// 读取配置时
$fp = fopen($lockFile, 'r');
flock($fp, LOCK_SH); // 共享锁:所有读进程不互斥
$config = readFromFile($fp);
flock($fp, LOCK_UN);
fclose($fp);

什么时候共享锁会出问题? 如果你在读取配置的过程中,依赖配置数据做后续写操作(如“读取余额→计算→写入新余额”),共享锁会导致读到脏数据,此时必须用排他锁。

场景 3:库存扣减 / 计数器(高一致性要求)

背景:秒杀场景下,商品库存数从 100 减为 0,多个请求同时读写。

推荐:排他锁(写锁)

  • 原因:这是一个经典的“读-修改-写”原子操作,即使是共享锁,也无法阻止多个进程同时读到库存=10,然后都扣减,导致库存为负数。
  • 更优方案:使用 Redis 原子操作(DECR)或数据库行锁,避免文件锁的复杂性。
// 使用 Redis 原子操作(推荐替代排他锁)
$stock = $redis->decr('stock:item_123');
if ($stock < 0) {
    $redis->incr('stock:item_123'); // 回滚
    echo '库存不足';
}

场景 4:日志写入 / 异步队列(写多读少)

背景:多个 worker 进程同时向一个日志文件追加内容。

推荐:排他锁(写锁)

  • 原因:一次只能有一个进程写入,防止数据交错乱行。
  • 优化:写锁本身会串行化,但日志场景天然需要顺序,如果性能不足,可改用 Redis list 或 Kafka 收集后再落盘。
$fp = fopen('/var/log/app.log', 'a');
flock($fp, LOCK_EX); // 排他锁
fwrite($fp, date('Y-m-d H:i:s') . " - " . $message . PHP_EOL);
flock($fp, LOCK_UN);
fclose($fp);

合理选用的决策流(场景决策树)

这个操作是读还是写?
   ├── 纯读(无后续写) → 共享锁(允许并发读)
   └── 写或读+写 → 进入2
2. 是否需要基于读取的结果来写?(如:读到旧值,修改后写回)
   ├── 是 → 必须用排他锁(保证事务隔离)
   └── 否 → 进入3
3. 写操作频繁吗?
   ├── 非常频繁(每秒多次)→ 排他锁(并发写必然冲突)
   └── 非常低(偶尔一次)→ 可用排他锁,或用“写时拷贝” + 共享锁(安全性较高,如配置文件)

常见误区与最佳实践

误区 正确做法
为了性能,所有读都用共享锁 但如果读后要写(如库存扣减),共享锁会导致超卖,这类场景必须用排他锁。
共享锁 = 完全不加锁 共享锁依然会阻止其他进程的写操作,适用于“读多写少”场景。
文件锁 (flock) 可以用于高并发数据库 文件锁受限于单机磁盘 I/O,高并发下推荐用 Redis、MySQL 行锁或分布式锁。
排他锁就是加锁,越短越好 排他锁持有期间会阻塞所有其他读/写,要尽量缩短加锁时间,不要在里面执行慢 SQL 或 sleep。

性能对比(文件锁为例)

场景 进程数 共享锁耗时 排他锁耗时
100 个进程同时读 100 极快(几乎同时完成) 很慢(串行执行) 纯读场景,共享锁快数十倍
100 个进程同时写 100 无法使用(写只能排他) 较慢(需排队) 无选择,必须排他
50 读 + 50 写 100 读快写慢,但写时读也等待 所有都等待 此时需要读写锁(或 PHP 中的 flock 无法区分,只能整体串行)→ 建议改用数据库或 Redis 实现读写锁

总结一句话

  • 如果并发场景是“多个线程/进程读取同一份数据,几乎不写” → 用共享锁(性能高)。
  • 如果并发场景是“任何写操作(包括读后写)” → 用排他锁(保证数据一致性)。
  • 如果同时有大量读和写(1:1 比例) → 不建议用文件锁,改用 Redis 读写锁或 MySQL 乐观锁。

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