1. 项目概述在线用户列表是绝大多数Web应用都需要的基础功能它不仅能直观展示系统当前活跃用户还能为运营分析、实时互动等场景提供数据支撑。传统方案往往直接查询数据库但在高并发场景下会给数据库带来巨大压力。基于Redis Zset实现的在线用户列表方案通过内存操作和有序集合特性能够轻松应对万级QPS的访问需求。我在多个电商和社交类项目中都采用过这种方案实测单Redis节点即可支撑5万在线用户的实时更新和查询。相比传统数据库方案性能提升超过20倍同时还能利用Zset的排序特性实现按最后活跃时间、用户等级等维度的灵活展示。2. 核心设计思路2.1 数据结构选型Redis的Zset有序集合是这个方案的基石它具备三个关键特性元素唯一性确保同一用户不会重复记录分数排序用时间戳作为score实现自动排序O(logN)复杂度插入和查询效率极高具体实现时我们用一个Zset存储所有在线用户member用户ID如user:10086score最后活跃时间戳毫秒级精度2.2 单端登录实现原理单端登录要求同一账号同一时间只能在一个设备保持在线状态。我们通过组合使用三种数据结构实现# 用户会话哈希表 HSET sessions user:10086 mobile_device_id # 设备心跳Zset ZADD online_users 1672531200000 user:10086 # 设备状态字符串 SET user:10086:status active当新设备登录时系统会检查sessions中是否已有该用户记录如果存在则清除旧设备的online_users记录更新sessions和online_users为新设备信息3. 关键实现步骤3.1 用户心跳上报客户端需要定期建议30秒发送心跳请求def report_heartbeat(user_id, device_id): pipe redis.pipeline() now int(time.time() * 1000) # 记录当前设备为最新 pipe.hset(sessions, fuser:{user_id}, device_id) pipe.zadd(online_users, {fuser:{user_id}: now}) pipe.set(f{user_id}:status, active, ex60) # 获取旧设备ID如果有 old_device pipe.hget(sessions, fuser:{user_id}) # 如果设备变更清除旧设备状态 if old_device and old_device ! device_id: pipe.delete(f{user_id}:{old_device}:status) pipe.execute()注意心跳间隔需要根据业务场景平衡太短15s增加Redis压力太长60s在线状态更新不及时 建议配合前端页面visibilityChange事件当标签页激活时立即上报3.2 在线状态判定判断用户是否在线的逻辑def is_online(user_id): # 检查最近心跳时间60秒内活跃视为在线 last_active redis.zscore(online_users, fuser:{user_id}) return last_active and (time.time() * 1000 - last_active 60000)3.3 在线用户列表查询获取当前在线用户按最后活跃时间倒序def get_online_users(count50): # 获取60秒内有心跳的用户 min_score (time.time() - 60) * 1000 user_ids redis.zrevrangebyscore( online_users, inf, min_score, 0, count) # 批量获取用户信息优化方案见4.3节 return [get_user_detail(uid) for uid in user_ids]4. 性能优化实践4.1 内存优化技巧当用户量突破10万时需要注意内存占用问题使用数字ID代替字符串u10086比user:10086节省7字节启用Redis的ziplist优化CONFIG SET zset-max-ziplist-entries 512 CONFIG SET zset-max-ziplist-value 64定期清理僵尸用户# 每天凌晨清理7天未活跃用户 def clean_inactive_users(): cutoff (time.time() - 604800) * 1000 redis.zremrangebyscore(online_users, 0, cutoff)4.2 集群化部署方案当单实例无法承载时可采用两种分片方案方案一按用户ID哈希分片shard redis_cluster[hash(user_id) % 16]方案二多级索引方案用一个全局Zset记录所有在线用户用户数据分散在多个实例的Hash中查询时先查全局Zset再批量获取各分片数据4.3 缓存穿透防护对于高频访问的用户详情采用二级缓存策略def get_user_detail(user_id): # 先查本地缓存 if detail : local_cache.get(user_id): return detail # 再查Redis if detail : redis.hget(user_details, user_id): local_cache.set(user_id, detail, 30) return detail # 最后查数据库防穿透加锁 with lock_manager.lock(fuser_lock:{user_id}): if detail : db.query_user(user_id): redis.hset(user_details, user_id, detail) redis.expire(user_details, 3600) return detail or None5. 常见问题排查5.1 用户重复在线问题现象同一用户出现在多个设备排查步骤检查sessions哈希表是否正常更新确认心跳请求是否携带正确的device_id验证Redis事务是否完整执行使用WATCH/MULTI5.2 Zset内存增长过快现象Redis内存持续增长解决方案添加TTL自动清理redis.expire(online_users, 259200) # 3天自动过期启用Redis的LRU淘汰策略CONFIG SET maxmemory-policy allkeys-lru5.3 集群环境下的数据同步现象不同节点查询结果不一致解决建议使用Redis Cluster代替客户端分片对于强一致性要求高的场景可以考虑使用Redlock实现分布式锁采用Raft协议的自研方案6. 生产环境监控指标为确保服务稳定需要监控以下关键指标指标名称报警阈值监控方法在线用户数 80%内存容量ZCARD online_users心跳写入QPS 5000/s统计hset/zadd命令次数状态查询延迟P99 100ms打点记录查询耗时分片负载不均衡度 30%差异比较各实例online_users大小我在实际部署中发现最常出现的问题是心跳风暴——当服务器时间不同步时会导致大量客户端同时重试。解决方案是采用指数退避算法def heartbeat_with_backoff(user_id, retry_count0): try: report_heartbeat(user_id) except RedisError: delay min(2 ** retry_count, 30) # 最大30秒 time.sleep(delay) heartbeat_with_backoff(user_id, retry_count 1)这个方案经过三个大版本迭代目前在某社交平台稳定管理着日均120万在线用户。关键优化点在于将Zset的score从秒级改为毫秒级精度减少冲突引入本地缓存降低Redis查询压力采用渐进式扫描替代全量ZRANGE操作