网站从一台服务器扩展到多台之后,很多站长会遇到同一个诡异现象:用户刚操作完,刷新一下就被踢回登录页;购物车里的商品时有时无;验证码明明填对了却总提示错误。这些问题的根源往往是同一件事——Session(会话数据)还留在单机上,而请求已经被负载均衡分发到了别的机器。
这篇文章要解决的就是这个问题:如何用 Redis 把分散在多台服务器上的会话数据集中存储,让任意一台机器都能读到同一个用户的登录状态。下面先讲清楚为什么会出问题,再给出 PHP 和 Node.js 两种环境的落地配置,最后补充负载均衡策略和线上故障排查方法。
为什么多台服务器会丢失登录状态
单机时代,用户首次访问时,程序会生成一份会话数据(用户 ID、购物车内容等)存在本机,同时给浏览器发一个只含会话 ID 的 Cookie。之后每次请求,程序拿 ID 回本机查数据,一切正常。
流量增大后加了服务器,前面挂负载均衡分摊请求,问题就来了:用户的第一个请求落在 A 机器,登录状态写进 A;第二个请求被分到 B 机器,B 上没有这份数据,只能视为未登录。”间歇性掉登录”就出现了——掉不掉,完全看请求落在哪台机器。
常见的应急方案是配置负载均衡的会话保持(Sticky Session),让同一用户始终落在同一台机器。它能暂时缓解症状,但隐患明显:某台机器宕机时,落在它上面的所有会话全部丢失;摘掉节点也得先”清空”上面的用户。所以会话保持只适合过渡,真正的解法是把会话数据从”各存各的”改成”集中存一份”,这正是 Session 共享要做的事。
用 Redis 集中存储会话的原理
Redis(内存键值数据库)是做 Session 共享最主流的选择:会话读取是典型的高频、小数据、低延迟场景,Redis 把数据放在内存里,单次读取通常在 1 毫秒以内,比读文件或查 MySQL(关系型数据库)快一个数量级。所有服务器把会话统一写入 Redis,任何一台机器收到请求都去同一个地方取数据,登录状态自然一致。
这样改造有三个直接收益:横向扩展不受限,应用服务器无状态,随时加减节点;故障影响面缩小,一台机器宕机只是切换节点,会话还在 Redis 里;过期策略由 Redis 的 EXPIRE 统一管理,不再依赖各机器的清理脚本。要注意的是,Redis 从此成为关键依赖,生产环境至少要配持久化和主从高可用,避免它变成新单点。

PHP 环境配置 Session 共享
PHP 的配置最简单,前提是装好 php-redis 扩展(Ubuntu 上 apt install php-redis),并有一台可访问的 Redis(本文以 VPS(虚拟专用服务器)自建实例为例)。打开 php.ini,修改两项:
session.save_handler = redis
session.save_path = "tcp://192.168.1.10:6379?auth=yourpassword&timeout=2.5"
把 IP 换成你的 Redis 内网地址,设了密码就带上 auth 参数,然后重启 PHP-FPM(PHP 进程管理器):sudo systemctl restart php8.2-fpm。
验证方法:部署一个探针页面,session_start() 后对 $_SESSION['counter'] 自增并输出 session_id(),放到负载均衡后面连续刷新。counter 持续递增、session id 不变,说明多台机器读到了同一份会话。也可以在 Redis 服务器执行 redis-cli keys 'PHPREDIS_SESSION:*' | head,确认会话键确实写进了 Redis。
WordPress 同样适用这套配置。如果你的站点还在单台虚拟主机上、尚未到多机架构,可以先了解 Hostease 的 VPS 主机(虚拟专用服务器),拿到独立环境后再做会话层改造会自由很多。

Node.js 环境配置 Session 共享
Node.js 的会话由 express-session 中间件管理,默认存在进程内存里,多进程或多机部署下同样会出错,正确做法是外接存储。安装依赖:
npm install express-session connect-redis redis
然后配置中间件:
const session = require('express-session');
const { createClient } = require('redis');
const RedisStore = require('connect-redis').default;
const redisClient = createClient({ socket: { host: '192.168.1.10', port: 6379 } });
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'change-me-to-a-long-random-string',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 1000 * 60 * 60 * 24 },
ttl: 86400
}));
resave: false 避免每次请求重写会话;saveUninitialized: false 不给匿名访客建会话,降低存储压力;ttl 是过期秒数,这里设 24 小时。重启后登录一次,在 Redis 上执行 redis-cli keys 'sess:*' 看到会话键,再启动第二个实例验证登录状态是否跨机器保持。

负载均衡策略与切换顺序
Redis 共享稳定后,负载均衡可以放开会话保持,轮询、最少连接这类纯流量策略都能正常使用。切换顺序很关键:先确认 Redis 共享稳定,再关掉会话保持;顺序反了用户会立刻大面积掉登录。
另一个容易踩的坑是 Cookie 域设置:多台节点必须给浏览器下发同一个域的会话 Cookie,否则浏览器会当成两个会话,WordPress 主机这类自带缓存层的环境尤其要核对这项配置。对比各节点返回的 Set-Cookie 响应头,确保 domain、path、name 三个属性完全一致。
常见故障排查
上线后仍偶发掉登录时,按以下顺序排查能覆盖大多数情况:
- 确认请求真的走 Redis:在某台 Web 服务器执行
redis-cli monitor | grep -i session后刷新页面,看不到 GET/SET 命令说明配置未生效,常见原因是 CLI 和 PHP-FPM 用了两份 php.ini,用php-fpm -i | grep save_path核对。 - 检查内存是否打满:
redis-cli info memory | grep used_memory_human对比 maxmemory,活跃用户多的站点建议 maxmemory-policy 用volatile-lru,并给业务缓存加过期时间,避免会话键被 LRU(最近最少使用淘汰算法)误删。 - 核对节点时钟:各机器时间相差超过 1 分钟会出现”这台有效、那台过期”的怪象,用 chrony 或 NTP(网络时间协议)同步,做法可参考这篇服务器时间同步与运维文章。
- 抓失败请求看 Cookie:用浏览器开发者工具或
curl -v复现掉登录操作,检查会话 Cookie 是否存在、各节点 Cookie 名是否一致(比如都叫 PHPSESSID)。
按这四步走完仍异常,大多属于应用代码层问题(如登录后手动重置了 session id),这时要看代码而不是基础设施。

总结与行动建议
多服务器环境下的登录状态问题,本质是”有状态应用”与”无状态分发”的矛盾,Session 共享把状态从应用节点剥离,是横向扩展必须迈过的坎。核心结论是:用 Redis 集中存储会话,配置成本低(PHP 两行配置、Node.js 一个中间件),收益却是全局的——加减服务器、宕机切换,用户都无感知。
落地建议按优先级排:第一,先在测试环境验证会话共享,确认多节点读取一致后再动生产;第二,给 Redis 配持久化和主从高可用,别让它成为新单点,若还在单机阶段,可以考虑先迁到支持灵活扩展的独立服务器;第三,关掉会话保持前先观察一天会话命中率,数据平稳再切换。如果你需要评估当前架构是否适合改造,先把并发量和部署方式整理出来,再决定从哪一层动手。