核心是用 last_activity 字段判断用户是否在线:若数据库中 last_activity 时间距当前时间不超过阈值(如5分钟),即视为在线;常见错误是直接用 time() - $user['last_activity']。

怎么用 last_activity 字段判断用户是否在线
核心就一条:不靠心跳长连接,只查数据库里最近一次操作时间。只要这个时间距现在不超过阈值(比如 5 分钟),就认为在线。
常见错误是直接用 time() - $user['last_activity'] 做判断,但没考虑 <code>last_activity 存的是 MySQL 的 NOW() 还是 PHP 的 time() —— 时区不一致会导致批量误判 offline。
- 统一用 UTC 时间存
last_activity,PHP 写入前调gmdate('Y-m-d H:i:s'),读取后用strtotime()转成时间戳再比 - 避免在 SQL 里用
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_activity) ,MySQL 函数执行开销高,且无法走索引 - 字段类型推荐
DATETIME(非TIMESTAMP),后者会自动转时区,容易和 PHP 层脱节
为什么不能只依赖 session 存活时间
PHP 默认 session 过期是服务器端控制的,session.gc_maxlifetime 设了 1440 秒,不代表用户“在线”状态能准确反映——用户关浏览器、网络中断、页面挂起,session 文件还在磁盘上没被回收。
更麻烦的是,session ID 可能被复用或未销毁(比如用户登出没调 session_destroy()),导致“已下线”却还显示在线。
立即学习“PHP免费学习笔记(深入)”;
- session 适合做登录态校验,不适合做实时在线状态依据
- 如果硬要用 session,必须配合前端定时发
/api/keepalive请求,并在每次请求里更新last_activity - 别把
$_SESSION['last_seen']当唯一依据,它可能被缓存、未写入、或跨请求丢失
UPDATE users SET last_activity = NOW() WHERE id = ? 该在哪儿执行
不是所有请求都值得更新。首页访问、静态资源加载、API 查询类接口,没必要每秒刷一次 last_activity;否则 DB 写压力陡增,且数据失真(用户其实早离开了)。
真正有意义的更新点只有两类:用户主动行为,和关键保活动作。
- 登录成功后立即更新
- 每次调用需鉴权的 API 接口(如
/api/profile、/api/messages)前更新 - 前端每 60 秒发一次轻量
/api/ping(只更新 DB,不返回数据),比轮询更可控 - 避开高频接口:比如搜索页每输入一个字就触发,这种绝对不能更新
last_activity
并发更新 last_activity 导致的脏写问题
多个标签页同时打开、前端重试机制、用户快速切换页面,都可能导致同一用户短时间内多条 UPDATE 并发执行。MySQL 默认隔离级别下不会丢数据,但可能造成不必要的 I/O 和锁等待。
更隐蔽的问题是:两个请求几乎同时读到旧的 last_activity,各自加 300 秒再写回,结果实际只推进了 300 秒,而不是最新时间。
- 用
UPDATE users SET last_activity = NOW() WHERE id = ? AND last_activity 加条件,避免无效更新 - 不要在事务里长时间 hold 住用户行锁,
last_activity更新必须快进快出 - 如果用 Redis 缓存在线状态,DB 更新后记得同步
SETEX user:online:{id} 300 1,但注意 Redis 和 DB 的最终一致性窗口
最常被忽略的点:前端没做防抖,visibilitychange + beforeunload 事件没监听,导致用户切走后还在续命。真实在线状态永远是“最后有效交互时间”,不是“最后看到页面的时间”。











