共享内存(shmop)适合高频读写但需手动加锁:它让Worker进程映射同一物理内存以避免序列化开销,但PHP shmop无原子操作,须用flock或信号量协调写入,否则易数据覆盖或读脏值。

共享内存(shmop)适合高频读写但需手动加锁
Workerman 多进程间共享大数据块,shmop 是最直接的底层方案——它让所有 Worker 进程映射同一块物理内存,避免序列化和复制开销。但 PHP 的 shmop 扩展不提供原子操作或内置锁,你必须自己用 flock 或信号量协调写入,否则极易出现数据覆盖或读到脏值。
常见错误现象:shmop_write 写入一半被另一个进程中断,导致后续 shmop_read 解析出乱码或截断数据;或者多个进程同时 shmop_open 同一个 key 却没检查返回值,误以为创建成功而实际打开失败。
实操建议:
- 固定分配足够大小的共享内存段(例如
shmop_open($key, 'c', 0644, 1024 * 1024)分配 1MB),避免频繁 resize - 每次写入前先用
flock($fp, LOCK_EX)锁住对应文件描述符(shmop_open返回的资源可配合tmpfile()模拟锁文件) - 数据结构尽量扁平,避免嵌套数组或对象;推荐用二进制协议(如 length + payload)存入,读取时按偏移解析
- 不要依赖
Worker::$shmCache——它是 Workerman 封装的简易缓存,底层仍走shmop,但无并发保护,不适合大数据块
Redis 作为中转更适合结构化大对象和跨机器扩展
当数据块超过几 MB,或需要支持故障恢复、过期淘汰、多节点部署时,redis 是更稳健的选择。它天然支持 SET/GET 二进制数据,且通过 pipeline 或 redis-bloom 等扩展还能做压缩与分片。
性能影响:单次 SET 10MB 数据在千兆内网延迟约 5–15ms,比 shmop 慢一个数量级,但省去了锁管理、内存泄漏排查、进程崩溃后共享内存残留等运维成本。
实操建议:
- 启用
redis.compression = lz4(PHP Redis 扩展 v5.3.7+),对 JSON 或序列化字符串压缩率可达 60%+ - 用
SET key value EX 300 NX带过期和原子写入,避免写入中途 Worker 挂掉导致脏数据滞留 - 大对象拆成 chunk 存储(如
data:12345:0,data:12345:1),配合GETRANGE按需加载,减少单次网络载荷 - 禁用
serialize处理器,直接set($key, $raw_binary_data),避免额外编码开销
TCP Socket 直传适用于 Worker 到 Worker 的点对点大数据推送
如果数据只在两个特定 Worker 之间流动(比如 Gateway → BusinessWorker 的原始音视频帧),用本地回环 TCP(127.0.0.1:2345)直连比经由 Redis 或共享内存更轻量。Workerman 本身基于 ReactPHP,stream_socket_client 和 stream_socket_server 可无缝集成。
容易踩的坑:stream_socket_send 默认阻塞,一次发不完 10MB 会卡住整个 EventLoop;粘包问题若没加长度头,接收端根本无法判断消息边界。
实操建议:
- 发送端必须实现分包逻辑:每包前 4 字节写
pack('N', $len)表示后续 payload 长度,包大小控制在 4KB–8KB - 接收端用
stream_socket_recvfrom循环读,维护 buffer 累积未完成包,直到收到完整 length + payload - 连接复用:Worker 启动时建立长连接池(如 5 个预连 socket),避免每次通信都
connect开销 - 超时设紧:
stream_set_timeout($socket, 1),防止某次卡死拖垮整个进程
别碰文件系统直读写——除非数据更新频率低于 1 次/秒
用 fopen('/tmp/shared.bin', 'c') 配合 flock 看似简单,但磁盘 IO 是瓶颈:10MB 文件写入 SSD 也要 20–50ms,且 flock 在 NFS 或某些容器环境下可能失效。Workerman 官方文档里提到的“文件锁方案”,仅适用于配置同步、开关切换等极低频场景。
真实问题案例:某用户用文件存 session 数据块,在 200 QPS 下 fwrite 出现大量 Resource temporarily unavailable 错误,本质是文件锁争用 + 磁盘队列堆积。
如果你非要用文件,请确认以下三点:
- 文件路径挂载在
tmpfs(内存文件系统),如/dev/shm/mydata.bin - 每次写入前
ftruncate清空再写,避免 append 导致碎片和长度误判 - 读取端用
file_get_contents加FILE_BINARY | FILE_NO_DEFAULT_CONTEXT,跳过流包装开销
真正难的不是选哪种方式,而是判断数据生命周期——共享内存适合“进程存活期内一直有效”的热数据;Redis 适合“需要跨进程、跨时间、带策略”的业务数据;Socket 直传只适合“一发一收、不落地、低延迟”的通道数据。混用这三者才是常态,比如用共享内存存计数器,Redis 存原始 blob,Socket 推送实时指令。


















