不能直接用exists判断用户在线,因为其仅检测key存在性,无法识别key存在但已过期或值失效的情况;必须通过lua脚本原子执行ttl判断(-2为离线、-1为异常、≥0为在线),并配合setex保证心跳写入的原子性,避免脏数据。

为什么不能直接用 EXISTS 判断用户在线?
很多同学一上来就用 EXISTS user:123 查 key 是否存在,但这样会漏判:如果用户刚下线、key 还没过期,或者 key 存在但对应的值已失效(比如只存了心跳时间戳但没校验是否超时),结果就不准。
真正可靠的在线判断必须同时检查 key 是否存在 + 值是否在有效期内,而这两步在客户端做会有竞态——比如查完存在、还没读值时 key 就过期了。所以得用 Lua 保证原子性。
EVAL 脚本里怎么安全读取并判断过期?
Redis 的 GET 和 TTL 本身不组合成原子操作,但 Lua 脚本能在一个执行周期内完成。关键不是“有没有值”,而是“值是否新鲜”。常见错误是只比对 TTL 返回值是否 > 0,其实 TTL 返回 -2(key 不存在)或 -1(永不过期)也要分别处理:
-
TTL返回 -2 → key 不存在 → 用户离线 -
TTL返回 -1 → key 存在但没设过期 → 通常不该出现,可视为异常或按离线处理 -
TTL返回 ≥ 0 → key 存在且未过期 → 用户在线
示例脚本:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
return redis.call('TTL', KEYS[1]) > 0 and 1 or 0 —— 注意这里用 TTL 而不是 GET,避免读取无效时间戳再自己算,更轻量。
心跳更新时为什么推荐用 SETEX 而非 SET + EXPIRE?
用两个命令更新状态,中间若出错(比如网络断开、Redis 写入失败),会导致 key 存在但无过期时间,后续所有 TTL 都返回 -1,用户永远“在线”。SETEX 是原子的,一次写入值+过期时间,不会残留无过期的脏数据。
- 正确:
SETEX user:123 30 "1712345678"(30 秒后自动删除) - 错误:
SET user:123 "1712345678"+EXPIRE user:123 30(两步可能割裂) - 如果业务需要存结构化数据(如最后登录 IP),建议用
HSET+EXPIRE,但必须确保两者在同一个 pipeline 或 Lua 脚本里执行
高并发下 EVAL 脚本性能瓶颈在哪?
脚本本身很快,但瓶颈常在 key 设计上。如果所有用户都用 user:{id} 作为 key,热点用户(比如直播间主播)的 key 会被高频访问,单个 Redis 实例扛不住。这时候不能靠加机器横向扩展,得拆 key:
- 把用户分片:
user:{id % 100}:{id},让请求分散到不同 key 空间 - 避免在脚本里做循环或复杂计算(比如遍历多个 key 判断“任一设备在线”),Redis 是单线程,脚本卡住会影响所有请求
- 脚本返回值尽量简单(0/1),别返回 JSON 字符串,序列化/反序列化消耗 CPU
上线前务必用 redis-benchmark -n 10000 -c 50 --eval 测真实吞吐,别只看单次耗时。










