FrankenPHP 1.3 worker 模式下需弃用原生 session,改用 Redis 手动管理会话:禁用 session_start(),统一 Cookie 参数,从 Cookie 提取 PHPSESSID,通过 Redis 读写序列化数据,并用 multi/exec 或 Lua 保证并发安全。

FrankenPHP 1.3 的 worker 模式下,原生 PHP session 无法跨实例共享,这是由其多进程常驻架构决定的——每个 worker 是独立进程,彼此内存隔离,session_start() 会各自初始化、写入、覆盖,导致登录态丢失、PHPSESSID 冲突、数据不一致。所谓“分布式会话”,不是配置开关就能启用,而是必须主动弃用内置 session 机制,改用统一外部存储(如 Redis)+ 手动生命周期控制。
下面分三块说清关键配置动作:
必须禁用 session_start(),改用手动 Redis 会话读写
所有入口脚本(如 public/worker.php)开头就加:
ini_set('session.use_strict_mode', 0); // PHP 8.4+ 必加,避免 ID 格式校验静默失败
// 彻底跳过 PHP session 扩展,不调用 session_start()、session_write_close() 等 然后自己实现:
立即学习“PHP免费学习笔记(深入)”;
- 从
$_COOKIE['PHPSESSID']或请求头Cookie:中提取会话 ID(前端需确保透传) - 用 Redis 读取
sess_{id}键(例如$redis->get("sess_$sid")) -
unserialize()还原数组(注意 ThinkPHP 5.1 等老框架只认 PHP 原生序列化格式) - 修改后
serialize($data)并setex("sess_$sid", $gc_maxlifetime, ...)写回 - 所有 worker 实例必须使用完全相同的 key 前缀(如
sess_)、TTL 值、序列化方式
全局统一 Cookie 参数,防止浏览器发多个 PHPSESSID
在每个入口脚本最顶部(任何输出前)、session_start() 调用前(哪怕你没调,也得提前设),强制设定:
session_set_cookie_params([
'lifetime' => 0, // 浏览器关闭失效
'path' => '/', // 统一路径
'domain' => '.example.com', // 注意开头的点,匹配子域名
'secure' => true, // HTTPS 下必须
'httponly' => true, // 防 XSS
'samesite' => 'Lax', // 推荐,兼顾安全与兼容
]);若走 Nginx 反代,还需确认:
-
proxy_set_header Cookie $http_cookie; -
proxy_pass_request_headers on;
否则后端收不到 Cookie,ID 提取失败。
Redis 连接与会话逻辑要线程安全,避免并发覆盖
FrankenPHP worker 是多线程常驻,同一进程内多个请求可能并发读写同一个会话 ID。建议:
- 使用
Redis::multi(Redis::MULTI)+exec()包裹读-改-写操作(即 CAS 模式) - 或直接用 Lua 脚本原子执行:先 get,再 setex,中间不被中断
- 不要依赖
session_regenerate_id()—— 它底层仍走 PHP session 扩展,已禁用 - 登出时手动删 Redis 键:
$redis->del("sess_$old_sid")
示例片段(精简):
$sid = $_COOKIE['PHPSESSID'] ?? bin2hex(random_bytes(16)); $raw = $redis->get("sess_$sid"); $data = $raw ? unserialize($raw) : []; // … 修改 $_SESSION 数据 … $redis->setex("sess_$sid", 1440, serialize($data)); // TTL 单位秒
本质上,这不是“配置”,而是一次会话控制权的移交:把状态管理从 PHP 内核交给 Redis,把 ID 生命周期从 session_* 函数交给你自己写的几行逻辑。只要所有 worker 实例连的是同一个 Redis、用同一套命名规则和参数,会话自然就分布且一致了。



















