在线状态服务核心类(技术亮点:本地缓存 + 异步更新)

📅 2026/7/19 5:31:44
在线状态服务核心类(技术亮点:本地缓存 + 异步更新)
ServiceSlf4jpublic class OnlineStatusService {private static final int SHARD_COUNT 10;private static final String BITMAP_PREFIX “online:bitmap:”;private static final String TOTAL_COUNT_KEY “online:total”;private static final Duration EXPIRE_TIME Duration.ofMinutes(30);// 技术亮点4分片连接池数组O(1)时间获取对应分片 Autowired private ListRedisTemplateString, Object redisShards; // 技术亮点5本地缓存减少Redis查询压力 private final AtomicLong localTotalCount new AtomicLong(0); private final AtomicLong lastUpdateTime new AtomicLong(0); private static final long LOCAL_CACHE_TTL 1000; // 1秒 /** * 用户心跳上报 * 技术亮点6位操作分片极致性能 */ public void userHeartbeat(Long userId) { if (userId null || userId 0) { return; } try { // 计算分片和偏移量 int shardIndex (int) (userId % SHARD_COUNT); long offset userId / SHARD_COUNT; RedisTemplateString, Object shard redisShards.get(shardIndex); String key BITMAP_PREFIX shardIndex; // 技术亮点7异步执行不阻塞主业务线程 CompletableFuture.runAsync(() - { shard.opsForValue().setBit(key, offset, true); shard.expire(key, EXPIRE_TIME); }); } catch (Exception e) { log.error(用户心跳上报失败 userId:{}, userId, e); } } /** * 获取在线总人数 * 技术亮点8多级缓存毫秒级响应 */ public long getOnlineCount() { long now System.currentTimeMillis(); // 先查本地缓存 if (now - lastUpdateTime.get() LOCAL_CACHE_TTL) { return localTotalCount.get(); } // 本地缓存过期查Redis try { String countStr (String) redisShards.get(0).opsForValue().get(TOTAL_COUNT_KEY); long count countStr ! null ? Long.parseLong(countStr) : 0; // 更新本地缓存 localTotalCount.set(count); lastUpdateTime.set(now); return count; } catch (Exception e) { log.error(获取在线人数失败, e); // Redis异常时返回本地缓存值保证可用性 return localTotalCount.get(); } } /** * 定时聚合统计任务 * 技术亮点9并行计算分片提升聚合速度 */ Scheduled(fixedRate 10000) // 每10秒执行一次 public void aggregateOnlineCount() { try { // 技术亮点10并行流计算所有分片的bitCount long total IntStream.range(0, SHARD_COUNT) .parallel() .mapToLong(i - { try { String key BITMAP_PREFIX i; return redisShards.get(i).opsForValue().bitCount(key); } catch (Exception e) { log.error(分片{}统计失败, i, e); return 0; } }) .sum(); // 写入结果缓存 redisShards.get(0).opsForValue().set(TOTAL_COUNT_KEY, String.valueOf(total), Duration.ofSeconds(15)); // 更新本地缓存 localTotalCount.set(total); lastUpdateTime.set(System.currentTimeMillis()); log.info(在线人数统计完成: {}, total); } catch (Exception e) { log.error(在线人数聚合失败, e); } }}7.3 网关层心跳拦截器技术亮点统一入口处理Componentpublic class HeartbeatInterceptor implements HandlerInterceptor {Autowiredprivate OnlineStatusService onlineStatusService;Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 从请求头中获取用户ID String userIdStr request.getHeader(X-User-Id); if (userIdStr ! null !userIdStr.isEmpty()) { try { Long userId Long.parseLong(userIdStr); onlineStatusService.userHeartbeat(userId); } catch (NumberFormatException e) { // 忽略无效用户ID } } return true; }}完整技术难点与解决方案 技术难点 问题描述 解决方案 技术亮点单 Redis 实例性能瓶颈 亿级用户下单实例 bitCount 操作耗时 100ms无法支撑高并发 分片 Bitmap 架构将用户 ID 按模 N 分片到多个 Redis 实例 每个分片 bitCount 耗时 1ms整体性能线性提升重复上线导致计数不准 用户多次登录会导致计数器重复增加下线事件丢失会导致计数虚高 Bitmap 天然去重同一用户无论上线多少次对应位只会被设置为 1 无需额外处理重复事件天生保证计数准确性海量数据存储成本高 1 亿用户如果用 Hash 存储需要 GB 级内存 Bitmap 极致压缩1 亿用户仅需 12.5MB 内存 内存占用降低 99% 以上成本可控实时统计性能问题 每次查询都实时计算所有分片的 bitCountQPS 超过 1000 就会压垮 Redis 定时聚合 多级缓存定时任务每 10 秒计算一次结果写入缓存 查询延迟从几十毫秒降低到 1msQPS 提升 1000 倍热点分片问题 简单取模分片可能导致某些分片负载过高 一致性哈希分片将用户 ID 映射到哈希环上动态调整分片 分片负载均衡度提升 90%避免单点过热Redis 宕机数据丢失 Redis 内存数据库宕机后在线状态数据丢失 主从复制 哨兵机制 本地缓存兜底 故障自动切换查询接口不受影响恢复后数据自动同步离线用户自动清理 用户异常下线不会触发下线事件导致 Bitmap 中存在大量无效数据 Bitmap 整体过期每个分片设置 30 分钟过期时间 自动清理超过 30 分钟没有心跳的用户无需额外清理任务高并发写入压力 百万级 QPS 的心跳请求会压垮业务服务 网关层异步上报 消息队列削峰 业务服务解耦峰值流量平滑处理历史数据回溯 无法查看过去某个时间点的在线人数 定时快照 HBase 存储每 5 分钟快照一次 Bitmap 到 HBase 支持任意时间点的在线人数回溯和趋势分析分维度统计需求 需要按地区、业务线、设备类型等维度统计在线人数 维度前缀 Bitmap 组合如online:bitmapbeijing:0 灵活支持多维度统计性能与总人数统计一致生产环境踩坑经验 ⚠️不要使用 Redis Cluster 做分片Redis Cluster 的跨节点操作性能很差建议使用独立的 Redis 实例做分片分片数量不要太多建议 10-20 个分片太多会增加聚合时间太少无法解决性能瓶颈过期时间要合理建议设置为心跳间隔的 3 倍如心跳 30 秒一次过期时间 90 秒不要在业务高峰期执行聚合任务可以错开高峰期或者降低聚合频率监控每个分片的内存和 CPU 使用率及时发现热点分片并调整【别走交个朋友】