从一个场景聊起
今天也来分享一个我们客户的案例。去年618大促期间,客户的App促销页面临时接到一个需求:实时显示"当前浏览人数"。这个数字每秒可能会更新数万次。如果用传统关系数据库,在10万级并发下会发生什么?
典型的错误示范如下:
// Java新手常见写法
public synchronized void addCount() {
int count = getFromMySQL();
updateMySQL(count + 1);
}
这段代码在高并发下会引发连锁反应。数据库连接池很快被打爆,超时错误接踵而至;同时,行锁竞争导致大量线程阻塞,死锁风险急剧上升;CPU使用率瞬间冲顶,系统性能彻底崩溃。
这,正是关系数据库ACID特性在高并发场景下反而成为负担的原因。
面对这种场景,Redis基于内存的原子操作和多样的数据结构,提供了更轻量级的解决方案。
Redis 对此问题的解决方案演进
阶段一:Hash,精确统计的起点
核心思路:用哈希表记录每个独立访客,适合对精度要求高、规模适中的场景。
// 将用户访问记录写入Hash
Jedis jedis = new Jedis("localhost", 6379);
String pageKey = "page_visit_count:examplePage"; // 页面访问key
String userId = "user_123"; // 登录用户
String visitorId = "visitor_abc123"; // 未登录访客
// HSET命令写入,value简化为1
jedis.hset(pageKey, userId, "1");
jedis.hset(pageKey, visitorId, "1");
// 统计访问量,HLEN直接获取字段数
long visitCount = jedis.hlen(pageKey);
System.out.println("页面访问人数:" + visitCount);
这种方式简单直接,实现容易,查询方便,精度也最高。
但随着访问页面增多,key会像滚雪球般膨胀,链表越来越长,内存占用迅速飙升,性能随之下降。因此更适合流量相对较小的页面,作为小规模的初步尝试。
阶段二:Bitmap方案,空间与性能的权衡
当用户量达到百万级别,Hash的空间效率就显得有些浪费了。Bitmap作为空间压缩专用的数据结构,巧妙利用32位int类型,即将每位独立使用,每位代表一个用户,节省高达32倍的空间。
实现逻辑很巧妙:登录用户直接用ID映射到位偏移量;未登录用户则通过哈希算法将随机字符串转为hash值再映射。比如用户ID为5,就对应第5位;随机字符串哈希值为10,就对应第10位。
// 使用Bitmap统计页面访问
Jedis jedis = new Jedis("localhost", 6379);
String pageKey = "bitmap_visit_count:examplePage"; // Bitmap访问key
String loginUserId = "5"; // 登录用户ID
String nonLoginUserKey = "random_str_123"; // 未登录用户的随机key
// 登录用户直接设置对应bit位
jedis.setbit(pageKey, Long.parseLong(loginUserId), true);
// 未登录用户先哈希再设置bit位
String hashValue = DigestUtils.md5DigestAsHex(nonLoginUserKey.getBytes());
long hashBitIndex = Long.parseLong(hashValue.substring(0, 16), 16) % Long.MAX_VALUE;
jedis.setbit(pageKey, hashBitIndex, true);
// BITCOUNT命令统计访问人数
long visitCount = jedis.bitcount(pageKey);
System.out.println("页面访问人数:" + visitCount);
这种方式的优势是内存占用极小,查询方便,甚至能直接判断某个具体用户是否访问过页面。
但缺点也很明显:多个用户可能映射到同一个bit位,导致统计结果略有偏差。此外,如果用户分布稀疏,比如用户ID达到1亿,就需要分配1亿个bit位,可能造成内存浪费。因此更适合用户分布相对密集的场景。
阶段三:HyperLogLog方案,海量数据的概率统计
核心突破:绝大多数场景不需要100%精确,用可控的误差换取内存和性能的飞跃。
HyperLogLog不再追求为每个用户精确计数,而是采用概率算法估算访问量的近似值。误差控制在0.81%以内,对实际业务影响微乎其微,却带来数量级的效率提升。
// 使用HyperLogLog统计页面访问
Jedis jedis = new Jedis("localhost", 6379);
String pageKey = "hyperloglog_visit_count:examplePage"; // HLL访问key
String userId = "user_456"; // 用户标识,可以是ID或其他唯一标识
// PFADD命令添加用户访问记录
jedis.pfadd(pageKey, userId);
// PFCOUNT命令获取估算人数
long approxVisitCount = jedis.pfcount(pageKey);
System.out.println("页面估算访问人数:" + approxVisitCount);
HyperLogLog的优势是内存占用极低(每个key仅12KB),无论用户量多大,空间复杂度始终是O(1)。缺点是牺牲了精确度,有0.81%的误差,且无法查询单个用户是否访问过。但对于千万级UV统计来说,这种精度完全可以接受。
方案选型:因地制宜方能制胜
这三种方案各有绝活。Hash倾向于精打细算,适合小规模精确统计;Bitmap是空间压缩的典范,适合用户分布密集的中等规模场景;HyperLogLog则是海量数据的概率统计数据结构,专为超高并发的大型网站而生。
实际项目中,选型需结合具体业务场景。流量小、要求精,用Hash;用户密、求省用Bitmap;数据大、容误差,用HyperLogLog。灵活组合,方能在高并发战场中游刃有余,准确高效地统计用户行为。
414

被折叠的 条评论
为什么被折叠?



