
这个问题本质上不是简单的计数问题而是高并发写入 低延迟读取 海量数据的综合挑战写入 QPS 极高用户上下线事件每秒可达百万级读取要求极低延迟毫秒级返回在线人数数据一致性要求不能出现明显的在线人数偏差成本可控不能为了统计而投入过多资源方案演进 我会从最简单的方案开始逐步演进到适合亿级用户的架构1️⃣ 初级方案数据库计数法万级用户// 伪代码public void userOnline(Long userId) {userDao.updateOnlineStatus(userId, true);}public long getOnlineCount() {return userDao.countByOnlineStatus(true);}问题每次统计都要全表扫描亿级数据下直接超时 ❌2️⃣ 中级方案Redis 计数器十万级用户// 伪代码public void userOnline(Long userId) {redisTemplate.opsForValue().set(“online:”userId, “1”, Duration.ofMinutes(30));redisTemplate.opsForValue().increment(“online:count”);}问题重复上线会导致计数器重复增加下线事件丢失会导致计数器不准单 Redis 实例无法支撑亿级用户 ❌3️⃣ 高级方案Redis Bitmap百万级用户// 伪代码public void userOnline(Long userId) {redisTemplate.opsForValue().setBit(“online:bitmap”, userId, true);}public long getOnlineCount() {return redisTemplate.execute((RedisCallback) conn - conn.bitCount(“online:bitmap”.getBytes()));}优势1 亿用户仅需 12.5MB 内存1 亿 bit 12.5MB问题单 Bitmap 在亿级用户下 bitCount 操作耗时仍会达到几十毫秒 ❌最终架构分片 Bitmap 定时聚合 多级缓存 ✅这是我在实际项目中使用过的、支撑过 3 亿 日活用户的方案image核心实现细节 用户 ID 分片将用户 ID 按模 10 分片到 10 个 Redis 实例每个分片最多存储 1000 万用户的在线状态每个分片的 bitCount 操作耗时 1ms在线状态更新// 伪代码public void userHeartbeat(Long userId) {int shard (int)(userId % 10);redisTemplate.opsForValue().setBit(“online:bitmap:”shard, userId/10, true);// 设置过期时间自动清理离线用户redisTemplate.expire(“online:bitmap:”shard, Duration.ofMinutes(30));}定时聚合统计// 伪代码每10秒执行一次Scheduled(fixedRate 10000)public void aggregateOnlineCount() {long total 0;for (int i0; i10; i) {total redisTemplate.opsForValue().bitCount(“online:bitmap:”i);}redisTemplate.opsForValue().set(“online:total”, total, Duration.ofSeconds(15));}查询接口public long getOnlineCount() {Long count redisTemplate.opsForValue().get(“online:total”);return count ! null ? count : 0L;}关键优化与边界处理 ⚡问题 解决方案 效果热点分片问题 采用一致性哈希替代简单取模 分片负载均衡度提升 90%实时性要求高 增加本地缓存每 1 秒更新一次 查询延迟 1ms精确性要求高 关键业务场景使用 HyperLogLog 辅助验证 误差控制在 0.81% 以内Redis 宕机 主从复制 哨兵机制 故障自动切换无数据丢失历史数据统计 每 5 分钟快照一次 Bitmap 到 HBase 支持任意时间点的在线人数回溯扩展能力 分地区 / 分业务统计增加维度前缀如online:bitmapbeijing:0用户在线时长统计结合 Redis Hash 记录用户最后上线时间异常用户检测统计单位时间内上下线次数识别机器人总结 亿级用户在线状态统计的核心思想是“分片处理 批量计算 结果缓存”用 Bitmap 解决海量数据的存储问题用分片解决单实例性能瓶颈用定时聚合解决实时统计的性能问题用多级缓存解决高并发查询问题这个方案在实际生产环境中支撑过 3 亿 日活、峰值 5000 万同时在线的场景查询延迟稳定在 1ms 以内完全满足互联网大厂的要求。现场模拟面试现场模拟面试面试官 “同学你好今天我们来聊一个场景设计题。假设现在我们有一个亿级用户的社交/IM系统需要实时统计用户的在线状态比如谁在线、当前总在线人数。要求支持高并发、低延迟而且资源成本要可控。你会怎么设计先说说你的整体思路。”候选人 “嗯……亿级用户的话首先想到不能用传统数据库频繁写。在线状态是典型的读多写少但变更频繁的场景可以用 Redis。大概思路是每个用户一个 key上线 set下线 del统计在线数用 keys *好像不太对……”面试官 “停keys * 在大数据量下会直接阻塞 Redis生产环境绝对是禁止的。你刚才说一个用户一个 key假设有一亿用户哪怕只有 1000 万同时在线那就是 1000 万个 keyRedis 内存受得了吗而且 key 过期管理也很麻烦。有没有更省内存的方案”候选人 “对应该用 Bitmap位图。给每个用户分配一个数字 ID上线就在对应的 bit 位置 1下线置 0。统计在线总人数直接 BITCOUNT 命令查询单个用户是否在线用 GETBIT。一亿用户才占用约 12.5MB 内存非常省。”面试官 “很好方向对了Bitmap 确实是这类问题的经典解法。那咱们继续深挖。你考虑过这三个实际痛点吗心跳与脏数据—— 用户直接断网或 APP 崩溃没有发下线请求Bitmap 里会一直「在线」。多端登录—— 同一个用户手机、PC 同时在线你只用一个 bit 怎么区分亿级用户 ID 不连续—— 如果用户 ID 是字符串或非连续数字Bitmap 就不好使了。”你看这三点怎么优化候选人 “嗯…心跳问题可以让客户端定时发心跳包比如每 5 分钟服务端收到后更新 Bitmap。但宕机的用户 Bitmap 还是 1所以需要后台定时任务比如每隔 10 分钟扫描 Bitmap与「最后心跳时间表」对比把超时的 bit 置 0。多端登录的话一个 bit 不够可以拆成多个 bit比如用 2bit 表示00 离线01 手机在线10 PC 在线11 多端在线。或者直接用另一个 Redis Set 存在线设备详情Bitmap 只负责快速统计和判存。ID 不连续问题可以做一层「全局 ID 映射」内部生成连续递增数字 ID和业务 ID 一一对应放在本地缓存或 Redis Hash 里。”面试官 “不错都对症下药了。那咱们再上升一个维度说说整体架构和高可用。我画个简图你边看边补充。” 整体架构示意image图释心跳记录 用 Sorted Setscore最后心跳时间戳可以快速找到超时用户。Bitmap 负责在线状态判断和全局计数。定时任务只扫 Sorted Set 中过期的用户去 Bitmap 置 0避免全量扫描。面试官 “那这个定时任务怎么扫才高效如果亿级用户同时在线心跳表也超大。”候选人 ⚡“Sorted Set 可以用 ZRANGEBYSCORE 分批拉取 score 小于当前时间 - 超时阈值的用户比如每次取 1000 条更新 Bitmap 后再删掉 Sorted Set 中的记录。这样每次只处理真正过期的用户压力可控。另外心跳写入用管道或异步批量写减少 Redis 连接开销。”面试官 “不错。那如果要输出「当前在线用户列表」或「在线用户详情分页」Bitmap 能扛吗”候选人 “Bitmap 其实不适合做列表分页它只擅长判存和计数。如果需要在线用户列表可以用 Redis Sorted Set 或 Set 存储在线用户 ID分页用 ZSCAN 或 SSCAN。但这样内存会大一些可以结合业务需要用 Bitmap 做快速统计和大批量查询用另一个轻量集合做少量列表展示并设置 TTL 心跳续期。”面试官 “Okay最后来个总结。在亿级用户在线状态场景下你的核心方案是”候选人 “总结一下一个中心、两个基本点中心以 Redis Bitmap 作为核心存储极致节省内存提供 O(1) 查询和 bitcount 统计。心跳维护通过 Redis Sorted Set 记录最后心跳时间定时任务增量清理超时 bit。ID 映射用连续数字 ID 映射业务 ID解决稀疏 ID 问题。多端扩展通过多 bit 位或辅助 Set 解决。高可用Redis Cluster 分片 哨兵主从切换避免单点。整体是一个读多写少、内存友好、可线性扩展的在线状态系统。”面试官 “非常清晰技术点都踩到了。记得实际落地还要考虑网络抖动导致心跳误判可加重试窗口、Redis 大 key 问题一亿 bit 的 Bitmap 不算大 key 但也要注意分片、以及一致性AP 模型允许少量不一致。好了这题你通过了回去等通知吧” 场景设计题如何统计亿级用户的在线状态补充版面试官您好我来补充一下这个方案的核心生产级代码和完整技术难点解决方案。核心生产级代码实现 7.1 Redis 配置类技术亮点自定义分片连接池ConfigurationEnableCachingpublic class RedisConfig {// 技术亮点1预定义10个分片连接池避免运行时动态创建Bean(“redisShard0”)public RedisTemplateString, Object redisShard0() {return createRedisTemplate(“192.168.1.10:6379”);}Bean(redisShard1) public RedisTemplateString, Object redisShard1() { return createRedisTemplate(192.168.1.11:6379); } // ... 省略其他8个分片的Bean定义 // 技术亮点2统一创建模板配置最优序列化方式 private RedisTemplateString, Object createRedisTemplate(String address) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(createConnectionFactory(address)); // 使用String序列化器避免JDK序列化的性能开销 StringRedisSerializer stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(stringSerializer); template.setHashValueSerializer(stringSerializer); template.afterPropertiesSet(); return template; } private LettuceConnectionFactory createConnectionFactory(String address) { String[] parts address.split(:); RedisStandaloneConfiguration config new RedisStandaloneConfiguration(); config.setHostName(parts[0]); config.setPort(Integer.parseInt(parts[1])); config.setDatabase(0); // 技术亮点3配置连接池参数优化高并发场景 LettucePoolingClientConfiguration poolConfig LettucePoolingClientConfiguration.builder() .poolConfig(new GenericObjectPoolConfig() {{ setMaxTotal(200); setMaxIdle(50); setMinIdle(10); setTestOnBorrow(true); }}) .commandTimeout(Duration.ofMillis(100)) .build(); return new LettuceConnectionFactory(config, poolConfig); }}