1 亿用户的好友关系如何秒级查询共同好友?这套方案让性能提升 100 倍!
“如何用Redis处理1亿级用户的好友关系?怎样秒级计算出任意两人的共同好友?”
小伙伴当场直接懵了。
在社交场景中,“共同好友”是用户关系链的核心功能(如微信、QQ)。但当用户量达到亿级时,数据量爆炸、实时性要求高、计算复杂度陡增,传统方案根本无法支撑。
一、问题本质分析:海量数据下的集合运算挑战
举个例子:假设每个用户有500个好友,1亿用户的好友关系总量是500亿条。
核心难点解析
数据规模爆炸:1亿用户 × 平均500好友 = 500亿关系数据
实时性要求:社交场景需毫秒级响应
内存成本压力:传统方案需TB级内存
二、三级解决方案:从青铜到王者
方案1:基础集合操作(青铜)
存储设计:用 Redis Set 实现“好友关系网”
核心思路:每个用户的唯一ID作为Redis的Key,对应的好友ID集合作为Value(使用Set结构存储)
优势:Set结构天然支持交集运算(
SINTER命令),计算共同好友只需一行代码:
# 用户1001的好友:2001、2002、2003SADD user:1001:friends 2001 2002 2003# 用户1002的好友:2001、2003、2004SADD user:1002:friends 2001 2003 2004# 计算共同好友(返回2001、2003)SINTER user:1001:friends user:1002:friends
缺点:
Redis的Set结构需要额外存储指针、哈希表等元信息,导致内存利用率低。(实测1亿关系需3TB+)
每个SINTER操作复杂度O(N*M),可能阻塞Redis线程。
青铜方案适合什么场景?
小规模应用:用户量在10万以下,好友数不超过500。
低频操作:每天查询次数小于1000次。
快速验证原型:MVP阶段验证功能可行性。
方案2:分片位图(黄金)
实现原理(用二进制位标记好友关系)
# 用户1001的好友:2001、2002(将好友ID转换为位偏移量)SETBIT user:1001 2001 1 # 第2001个二进制位设为1SETBIT user:1001 2002 1# 用户1002的好友:2001、2003SETBIT user:1002 2001 1SETBIT user:1002 2003 1# 计算共同好友(返回2001)BITOP AND common_friends user:1001 user:1002BITCOUNT common_friends # 统计共同好友数量
优化效果:
内存降低98%(1亿用户仅需125MB)
致命缺陷(BITOP的软肋):
但BITOP不支持跨节点操作
BITOP限制:要求所有参与运算的Key必须在同一个Redis节点。
Redis集群规则:不同Key可能分布在不同的机器上(根据CRC16哈希分片)。用户A和用户B的位图若在不同机器,无法直接计算共同好友!
优化效果(为什么能省98%内存?)
内存计算
原始Set结构:
存储用户1001的2001、2002两个好友需要:Set结构元数据:16字节
每个元素占用32字节(Redis用哈希表存储,包含指针、哈希值等)
→ 总内存:16 + 2*32 = 80字节位图结构:
存储到第2002位需要2002/8 ≈ 250字节(每字节存8位)
即使用户只有2个好友,仍需占用250字节
看起来位图更浪费?实则相反!
关键技巧:当好友ID范围连续时(比如用户ID从1开始自增),位图内存优势才会爆发:
# 用户1001有好友1~100000(10万个好友)# Set结构内存:16 + 100000*32 ≈ **3.05MB**# 位图内存:100000/8 = **12.5KB**(节省99.6%!)
解决方案(妥协的艺术)
强制分片绑定:
将用户ID和好友ID映射到固定范围(比如前100万ID在节点1,后续在节点2)
通过哈希规则确保用户和好友位图在同一节点
代价:牺牲扩展性,扩容需迁移大量数据
客户端合并计算:
从不同节点拉取两个用户的位图数据到客户端
在本地内存执行按位与运算
代价:网络传输消耗大
黄金方案适合什么场景?
好友ID范围集中:比如用户ID是自增数字,且范围可控(如不超过1000万)
高频读取低频写入:位图适合批量更新(如每日同步一次好友关系)
垂直分片架构:业务能接受用户按ID范围分区存储
方案3:分层分片架构(王者)
实现原理(快递分拣式计算)
为什么能解决跨节点问题?
场景1:两人数据在同一节点(简单模式)
用户A(ID 1001)和用户B(ID 2002)通过哈希分片规则落在同一个Redis节点。
直接在该节点执行
SINTER或BITOP,无需跨机器通信,延迟低于10ms。
场景2:两人数据在不同节点(地狱模式)
分阶段计算:
节点1计算用户A的好友列表,节点2计算用户B的好友列表。
各自将好友ID列表返回给路由层(如返回[2001,2002]和[2001,2003])。
合并求交集:
路由层在内存中对比两个列表,找出共同好友(2001)。
牺牲部分性能(100ms内)换取跨节点能力。
核心代码实现
1. 分片路由算法
public String getShardKey(Long userId1, Long userId2) {int shard = (userId1.hashCode() & userId2.hashCode()) % 1024;return "common_friends_shard_" + shard;}
2. Lua脚本原子计算
-- 分片内计算交集local key1 = KEYS[1]local key2 = KEYS[2]local tempKey = "temp_" .. key1 .. "_" .. key2redis.call('SINTERSTORE', tempKey, key1, key2)local result = redis.call('SMEMBERS', tempKey)redis.call('DEL', tempKey)return result
3. 跨分片合并
def get_common_friends(user1, user2):shard_keys = calculate_shards(user1, user2)results = []# 并行查询所有分片withThreadPoolExecutor() as executor:futures = [executor.submit(redis_cluster.execute, shard, 'SINTER', user1, user2)for shard in shard_keys]results = [f.result() for f in futures]# 合并最终结果return list(set().union(*results))
落地技巧(腾讯工程师内部配置)
分片规则:
使用一致性哈希算法,扩容时仅迁移25%数据,避免全量抖动。
# Python伪代码:根据用户ID计算分片def get_shard(user_id):return crc32(user_id) % 16384 # 16384个哈希槽
将常用操作(如好友交集+结果缓存)预加载到Redis,减少网络传输。
-- Lua脚本:计算并缓存共同好友local result = redis.call('SINTER', KEYS[1], KEYS[2])redis.call('SET', 'cache:'..KEYS[1]..KEYS[2], result, 'EX', 3600)return result
性能对比(1亿用户实测)
布隆过滤器+分片实现跨节点共同好友计算?MySQL要坐不住了!Vitess之父Sugu“投敌”Postgres造新数据库,这次真要掀翻桌子?
为什么DeepSeek火之后,人们想到的是大量裁员,而不是实行上三休四?
苹果“痛下杀手”弃Java,用自家语言Swift重写关键服务:内存减90%,性能增40%!
我的很多DBA朋友,都消失了...
3000亿芯片巨头海光信息拟合并900亿超算龙头中科曙光,国产算力产业的“王炸组合”
1000T的文件怎么能快速从南京传到北京?最佳方案你肯定想不到
麻了,领导让我写个脚本,把700T数据库全部迁移到别的机房,带宽不超过2Mbps
号外!《核心系统分布式数据库选型指南》电子书(附下载)正式上线
解锁数据架构现代化密码,《实时数仓选型指南》电子书(附下载)正式上线啦