php数组本身非线程安全,但传统fpm/cgi模式因进程隔离而无竞态;风险存在于apcu、swoole协程、redis等共享内存场景,需原子操作或加锁保障并发安全。

PHP 数组本身不是线程安全的,但在传统 PHP 运行模式(如 Apache mod_php 或 CGI)中,并发访问数组通常不会引发数据竞争——因为每个请求运行在独立进程或隔离的 FPM worker 中,内存不共享。真正的风险出现在共享内存场景下,比如使用 apcu、shmop、Redis 或 Swoole 等扩展实现跨请求/协程的数据共享时,对数组结构的并发读写若缺乏同步机制,极易导致数据丢失、结构损坏或逻辑异常。
多请求共用 APCu 数组时的竞态问题
APCu 提供用户缓存,支持存储数组,但其操作(如 apcu_fetch + apcu_store)不是原子的。例如多个请求同时执行“读取计数器 → 加1 → 写回”,可能全部读到旧值,最终只+1而非+n:
- 请求 A 读到
['count' => 5] - 请求 B 也读到
['count' => 5] - A 写入
['count' => 6] - B 写入
['count' => 6](覆盖 A 的结果)
解决方法:用 apcu_inc() 原子增减整数;对复杂数组操作,需配合 apcu_cas() 实现乐观锁,或改用支持事务的后端(如 Redis)。
Swoole 协程中全局数组的误用风险
Swoole 的协程是轻量级用户态线程,共享同一进程内存。若在协程间直接读写全局变量或静态属性中的数组(如 self::$cache = []),会出现典型的竞态条件:
立即学习“PHP免费学习笔记(深入)”;
- 协程 A 执行
$arr[] = $val,内部先获取长度再赋值,中间被协程 B 中断 - B 同样追加元素,可能覆盖 A 的索引或导致键重复
- 数组结构(如
zend_array的 hash table)可能因并发修改而进入不一致状态
建议:协程内避免共享可变数组;必须共享时,使用 Swoole\Coroutine\Channel 传递数据,或用 Swoole\Lock 加锁保护临界区,或改用线程安全的存储如 Redis。
Redis 存储 PHP 数组时的序列化陷阱
将 PHP 数组存入 Redis 常用 serialize() 或 json_encode(),但这只是“伪并发安全”:
- 读-改-写流程仍非原子:先
GET,再 PHP 解码、修改、编码、SET,期间其他客户端可能已更新该 key -
json_encode()会丢失 PHP 数组的键类型(如整数键转为字符串)、丢弃对象和资源 -
serialize()格式不跨语言,且反序列化存在安全风险(慎用于不可信数据)
更稳妥做法:用 Redis 原生命令操作结构化数据,例如用 HINCRBY 增减哈希字段、LPUSH/RPOP 操作列表,避免在应用层做“全量读写”。必要时结合 EVAL 脚本保证原子性。
FPM 场景下文件或数据库缓存的隐式共享
看似无共享的 FPM 模式,也可能因外部存储引入并发问题:
- 多个 worker 进程同时写同一个 JSON 缓存文件,可能造成内容截断或格式错乱
- 多个请求更新数据库同一行的 JSON 字段,未加行锁或版本号,导致后写者覆盖前写者修改
应对策略:文件操作使用 flock() 排他锁;数据库更新采用 SELECT ... FOR UPDATE 或乐观锁(如检查 updated_at 时间戳);优先使用支持原子操作的存储引擎(如 MySQL 的 JSON_SET() 函数)。
不复杂但容易忽略:并发安全不取决于数组本身,而取决于它所处的上下文是否共享、操作是否原子、以及是否有同步保障。从设计阶段就明确数据的作用域和生命周期,比事后加锁更有效。











