
php 信号量(sem_get)本身是进程级的系统资源锁,它能跨进程生效,但浏览器同源策略和 http 缓存机制(尤其是 chrome 的强制缓存行为)会导致并发请求被拦截或复用响应,造成“锁失效”的假象。本质问题不在 php,而在客户端缓存干扰。
php 信号量(sem_get)本身是进程级的系统资源锁,它能跨进程生效,但浏览器同源策略和 http 缓存机制(尤其是 chrome 的强制缓存行为)会导致并发请求被拦截或复用响应,造成“锁失效”的假象。本质问题不在 php,而在客户端缓存干扰。
PHP 的 sem_get / sem_acquire 是基于系统 IPC(System V 信号量)实现的底层进程同步机制,其作用域是服务器端的多个 PHP 进程(如 FPM worker)之间,而非浏览器端。这意味着:只要两个请求最终由不同的 PHP 进程(或线程)处理,信号量就能正确互斥;但如果浏览器因缓存提前返回了旧响应(例如 304 Not Modified 或直接复用内存缓存),第二个请求甚至根本未抵达 PHP 层——自然不会触发 sem_acquire,也就谈不上“加锁失败”。
你观察到“仅在不同浏览器中有效、同浏览器多标签页无效”,正是典型缓存干扰现象。Chrome 在无明确缓存控制头时,对重复 GET 请求(尤其无查询参数、无 POST 体)可能启用启发式缓存(heuristic caching),导致第二次请求被浏览器直接拦截并返回前一次响应,完全绕过 Nginx → PHP-FPM 链路。
✅ 正确做法:强制禁用客户端缓存 + 确保请求唯一性
在脚本开头添加严格的 HTTP 缓存控制头:
<?php // 禁用所有缓存:禁止存储、禁止复用、强制校验
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Expires: 0');
$semaphore_key = 25;
$semaphore_max = 1;
$semaphore_permissions = 0666;
$semaphore_autorelease = 1;
$semaphore = sem_get($semaphore_key, $semaphore_max, $semaphore_permissions, $semaphore_autorelease);
if (sem_acquire($semaphore, true) === false) {
http_response_code(423); // Locked
echo "Locked: another request is running";
exit();
}
echo "ok\n";
sleep(30);
sem_release($semaphore);⚠️ 注意事项:
- 不要依赖 $_SERVER['HTTP_CACHE_CONTROL'] 判断缓存状态:该字段由客户端发送,不可信且不具约束力;
- 避免仅用 meta http-equiv:HTML meta 标签对 AJAX 或直接 URL 访问无效,必须使用 PHP header() 发送真实响应头;
- GET 请求天然易被缓存:若业务允许,建议改用 POST 方法(浏览器默认不缓存 POST 响应),并配合 CSRF Token 提升安全性;
- 信号量非万能:它不解决分布式部署下的并发问题(需 Redis 锁等);单机高并发下也需注意信号量资源泄漏(务必配对 sem_release,建议用 register_shutdown_function() 做兜底);
- 调试技巧:用浏览器开发者工具 → Network 面板查看每个请求的 Size 列(显示 (from memory cache) 或 (from disk cache) 即为缓存命中);同时检查 Response Headers 是否含 X-Powered-By 等 PHP 特征头——若缺失,说明请求根本未到达 PHP。
总结:PHP 信号量工作正常,问题出在浏览器“自作聪明”的缓存行为。通过强制清除客户端缓存策略,并辅以合理的请求方法与错误码(如 423 Locked),即可让同浏览器多标签页的真实并发请求正确进入 PHP 互斥逻辑,实现预期的单实例执行控制。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











