本文目录导读:

在 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 乐观锁。