@Service
@Slf4j
public 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 List<RedisTemplate<String, 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; RedisTemplate<String, 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 网关层心跳拦截器(技术亮点:统一入口处理)
@Component
public class HeartbeatInterceptor implements HandlerInterceptor {
@Autowired
private 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% 以上,成本可控
实时统计性能问题 每次查询都实时计算所有分片的 bitCount,QPS 超过 1000 就会压垮 Redis 定时聚合 + 多级缓存:定时任务每 10 秒计算一次结果,写入缓存 查询延迟从几十毫秒降低到 < 1ms,QPS 提升 1000 倍
热点分片问题 简单取模分片可能导致某些分片负载过高 一致性哈希分片:将用户 ID 映射到哈希环上,动态调整分片 分片负载均衡度提升 90%,避免单点过热
Redis 宕机数据丢失 Redis 内存数据库,宕机后在线状态数据丢失 主从复制 + 哨兵机制 + 本地缓存兜底 故障自动切换,查询接口不受影响,恢复后数据自动同步
离线用户自动清理 用户异常下线不会触发下线事件,导致 Bitmap 中存在大量无效数据 Bitmap 整体过期:每个分片设置 30 分钟过期时间 自动清理超过 30 分钟没有心跳的用户,无需额外清理任务
高并发写入压力 百万级 QPS 的心跳请求会压垮业务服务 网关层异步上报 + 消息队列削峰 业务服务解耦,峰值流量平滑处理
历史数据回溯 无法查看过去某个时间点的在线人数 定时快照 + HBase 存储:每 5 分钟快照一次 Bitmap 到 HBase 支持任意时间点的在线人数回溯和趋势分析
分维度统计需求 需要按地区、业务线、设备类型等维度统计在线人数 维度前缀 + Bitmap 组合:如online:bitmap🇨🇳beijing:0 灵活支持多维度统计,性能与总人数统计一致
生产环境踩坑经验 ⚠️
不要使用 Redis Cluster 做分片:Redis Cluster 的跨节点操作性能很差,建议使用独立的 Redis 实例做分片
分片数量不要太多:建议 10-20 个分片,太多会增加聚合时间,太少无法解决性能瓶颈
过期时间要合理:建议设置为心跳间隔的 3 倍,如心跳 30 秒一次,过期时间 90 秒
不要在业务高峰期执行聚合任务:可以错开高峰期,或者降低聚合频率
监控每个分片的内存和 CPU 使用率:及时发现热点分片并调整
【别走,交个朋友】