ScyllaDB社交App双向好友关系数据建模方案咨询
ScyllaDB双向好友关系生产级建模方案
你的现有单表方案核心问题是为了省冗余牺牲了核心路径的查询性能,在ScyllaDB这类宽列存储中,反范式冗余是换取低时延的标准做法,完全符合你允许适度冗余的设计目标。以下方案可实现所有访问模式单分区命中,无跨节点查询风险,写操作通过原子批保证一致性。
核心表结构设计
-- 表1:用户视角关系表,支撑好友列表、收/发好友申请查询 CREATE TABLE IF NOT EXISTS user_relations ( user_id text, rel_type tinyint, -- 枚举值:0=已确认好友, 1=收到的待处理申请, 2=发出的待处理申请 peer_id text, created_at timestamp, updated_at timestamp, PRIMARY KEY (user_id, rel_type, peer_id) ) WITH CLUSTERING ORDER BY (rel_type ASC, peer_id ASC); -- 表2:关系全局索引表,核心解决双用户关系查询的性能问题 CREATE TABLE IF NOT EXISTS relation_lookup ( -- 生成规则:将两个用户ID按字典序升序排列后用固定分隔符拼接,例:uid1 < uid2 则值为 uid1:uid2,保证任意传参顺序都命中同一条记录 lookup_key text, uid_a text, uid_b text, status tinyint, -- 枚举值:1=uid_a向uid_b发了待处理申请, 2=uid_b向uid_a发了待处理申请, 3=双方为好友 requester text, req_time timestamp, accept_time timestamp, PRIMARY KEY (lookup_key) );
所有访问模式实现(均为单请求、单分区查询)
- 查询指定用户好友列表:执行CQL
SELECT peer_id, updated_at FROM user_relations WHERE user_id=? AND rel_type=0;,直接命中单分区聚类列,无需过滤。 - 查询指定用户收到的好友申请:执行CQL
SELECT peer_id, created_at FROM user_relations WHERE user_id=? AND rel_type=1;,单分区命中,性能和原有方案一致。如果后续需要支持查询用户发出的好友申请,直接查rel_type=2的聚类即可,无需改表。 - 查询两用户关系状态:
- 先对传入的两个用户ID做字典序排序,按规则拼接生成
lookup_key - 执行CQL
SELECT status, requester FROM relation_lookup WHERE lookup_key=?;,单行单分区查询,无任何IN条件,时延稳定在亚毫秒级 - 结果判断逻辑:
- 无返回记录:双方无任何关联
- status=3:双方为好友
- status=1/2:存在对应方向的待处理申请,
requester字段直接返回申请发起方ID
- 先对传入的两个用户ID做字典序排序,按规则拼接生成
所有写操作实现(均用LOGGED BATCH保证跨表原子性,无中间不一致状态)
ScyllaDB的LOGGED BATCH可以保证跨分区、跨表操作的原子性,批内都是简单写的场景下性能损耗不到10%,完全适配高TPS要求。
- 发送好友申请
- 对申请发起方、接收方ID排序生成
lookup_key,判断两个ID的顺序确定status取值 - 原子批写入3条记录:
- 写入
relation_lookup对应lookup_key的记录,填充status、requester、req_time字段 - 写入
user_relations:user_id=接收方ID,rel_type=1,peer_id=发起方ID,填充created_at - 写入
user_relations:user_id=发起方ID,rel_type=2,peer_id=接收方ID,填充created_at
- 写入
- 对申请发起方、接收方ID排序生成
- 拒绝好友申请
- 对两个用户ID排序生成
lookup_key - 原子批删除3条记录:
- 删除
relation_lookup中对应lookup_key的记录 - 删除
user_relations中user_id=接收方、rel_type=1、peer_id=发起方的记录 - 删除
user_relations中user_id=发起方、rel_type=2、peer_id=接收方的记录
- 删除
- 对两个用户ID排序生成
- 同意好友申请
逻辑比原有方案更简单,不需要额外的状态更新兼容:- 对两个用户ID排序生成
lookup_key - 原子批执行5个操作:
- 更新
relation_lookup对应记录的status=3,填充accept_time - 删除
user_relations中接收方下rel_type=1对应发起方的待处理记录 - 删除
user_relations中发起方下rel_type=2对应接收方的待处理记录 - 写入
user_relations:user_id=接收方,rel_type=0,peer_id=发起方,填充updated_at - 写入
user_relations:user_id=发起方,rel_type=0,peer_id=接收方,填充updated_at
- 更新
- 对两个用户ID排序生成
- 删除好友关系
- 对两个用户ID排序生成
lookup_key - 原子批删除3条记录:
- 删除
relation_lookup中对应lookup_key的记录 - 删除
user_relations中user_id=第一个用户、rel_type=0、peer_id=第二个用户的好友记录 - 删除
user_relations中user_id=第二个用户、rel_type=0、peer_id=第一个用户的好友记录
- 删除
- 对两个用户ID排序生成
方案对比原有设计的优势
- 核心的双用户关系查询从跨节点多分区IN查询变为单分区单行查询,彻底消除性能抖动,P99时延可稳定在1ms以内
- 所有写操作为固定逻辑的原子批,没有复杂的条件判断,出错概率极低
- 额外覆盖了“查询发出的好友申请”的潜在需求,无额外改造成本
- 不依赖Materialized View,避免了MV带来的一致性延迟、运维复杂度问题
- 冗余存储成本极低:
relation_lookup表仅存关系元数据,千亿级关系规模下存储开销也完全可控
内容的提问来源于stack exchange,提问作者JonasLevin
相关产品推荐
相关产品推荐

