../rs

用户关系系统设计方案

Published:

design

用户关系系统设计方案

用户间存在关注,被关注,好友(互相关注)三种关系。

背景说明

名词解释

关联数据

  1. 有关注行为的用户占比 40%, 这些用户平均关注 7.9 个用户。
  2. 被关注的用户占比约 35%,这些用户平均拥有 9.2 个粉丝。
  3. 既有关注行为也有粉丝的用户约 26%,这些用户平均关注 8.3 个用户,平均拥有 10.4 个粉丝。

关系枚举表

下文以 -> 表示关注,<- 表示被关注, <-> 表示互相关注,-- 表示无关系

A BB A
A -- B----
A -> B-><-
B -> A<-->
A -> B, B -> A<-><->

业务场景

  1. 关注,用户 A 关注用户 B。
  2. 取关,用户 A 取消关注 用户 B。
  3. 获取用户 A 的关注列表,并获取用户 C 与该关注列表的关系状态。
  4. 获取用户 A 的粉丝列表,并获取用户 C 与该粉丝列表的关系状态。
  5. 获取用户 A 的好友列表。

业务约束

技术方案

基于不同的用户量级设计相关的方案, 百万级,千万级,亿级,十亿级。 方案里涉及数据删除均为逻辑删除。

单表方案

关注产生正反两条记录, 数据量为关注行为的两倍。

数据表设计

关注表 followers

idsubjectobjecttsstatus
1102016558841505922
1201016558841505921
2103016558842505923
3301016558843505923

索引

业务操作

// 查询当前用户关系
rs = `followers where subject = a and object = b limit 1;
if rs { 
  if rs.status is 1 { // 被关注,需要变成好友
    `update followers status = 3 where subject = a and object = b`
    `update followers status = 3 where subject = b and object = a`
  }
} else {
  `insert followers(a b 2)`
  `insbrt followers(b a 1)`
}

// 查询用户 a b 关系
rs = `followers where subject = a and object = b limit`;

if follower.status is 3 { // 好友关系
  `del follower`
  `update status = 2 where subject = follower.object and object = follower.subject`
} else if follwer.status is 2 {
  `del follower`
  `del where subject = follower.object and object =  follower.subject`
}
// 关注存在上限,翻页可以用 offset limit 方式。
where subject = a and status >= 2 order by ts desc offset x limit y
// 粉丝列表翻页需要采用 search after 机制
where subject = a and status = 1 and ts < ? order by ts desc limit y

// where subject = a and status = 1 and ts < ? and id < ? order y ts desc, id desc limit y 
// 好友存在上限,翻页可以用 offset limit 方式。
where subject = a and status = 3 order by ts desc offset x limit y
where subject = a and object in (b,c,d)

补充说明

方案适用于百万用户量级,也就是最终单表数据量在 2000 万 以下,如果数据库配置够高,最终数据量在 2亿 左右也适用。

双表方案

采用关注表(事实表)与好友表(维度表)组合。 关注行为在关注表产生一条数据,根据情况决定好友表是否要产生数据。

a b 为好友的情况下,好友表维护一条记录

数据表设计

关注表, followers

idsubjectobjectts
110201655884150592
120101655884150592
130401655884150592

索引

好友表

idsubjectobjectts
110201655884150592

索引

业务操作


fans = `where subject = b and object a`
if not fans { // b 未关注 a
  `insert followers(a b)`
} else {
   min = min(a, b)
   max = max(a, b)
  `insert friends(min, max)` // 保证 subject < object, 可保证维护好友关系只需要一条记录
}

min = min(a, b)
max = max(a, b)
friend = `where subject = min and object = max`;

if friend {
  `del friend`;
  `del followers where subject = a and object = b`;
} else {
  `del followers where subject = a and object = b`;
}

followers where subject = a order by ts desc offset x limit y;
followers where object = a and ts < ? order by ts desc limit y;
friends where ts < ? and (subject  = a or object = a) order ts desc limit y;
rs = followrs where (subject = a and object in (b,c,d)) or (subject in (b,c,d) and object = a)

// 业务侧 merge

补充说明

此方案与单表方案的适用数据量类似。 好友表的设计可以采取好友关系产生两条记录的方式实现,查询好友列表的性能稍优。

关注表分表方案

当用户量进一步增加,单表预期数据量超过阈值时,需要考虑分表的方案。

分表指导原则

关注表分表,分表规则按照 subject hash 即可,由于用户存在关注上限,按 subject uid hash 分表之后数据分布是比较均匀的。 由于关注表按 subject uid 分表了,查询用户的粉丝列表就不能通过关注表来查询了,需要引入新的粉丝表。粉丝表按照 subject uid 分表即可,粉丝表与关注表的不同之处在于,一个用户的粉丝是没有上线的,需要考虑部分粉丝量特别大的用户导致的数据倾斜问题,可以考虑针对这类特殊的用户维护单独的粉丝表。

关注表 followers

一次关注产生一条关注记录

idsubjectobjecttsstatus
1102016558841505922
210020016558841505922

索引

粉丝表 fans

idsubjectobjectts
120101655884150592
22001001655884150592

粉丝表是基于关注表的维度表,数据一致性细节此处不展开。

业务操作

fans = `followers where subject = b and object = a limit 1;
if not fans { // 未被关注
   `insert followers(a, b, 2)`

   `insert fans(b, a)`; // 非事务,需要考虑一致性问题
} else { // 已关注,需要成为好友
    `insert followers(a, b, 3)`;

    `insert fans(b, a)`; // 非事务,需要考虑一致性问题

    `update fans set staus = 3 where subject = b and object = a; // 非事务,需要考虑一致性问题
}
follower = `follows where subject = a and object = b limit 1`;

if follower.status is 2 { // 关注
  `del follower` ; 

  `del fans where subject = follower.object and object = follower.subject` ; // 非事务,需要考虑一致性问题
} else if follower.status is 3 { // 好友
  `del follower` ;

  `del fans where subject = follower.object and object = follower.subject` ; // 非事务,需要考虑一致性问题

  `update followers set status = 2 where subject = b and object = a` ; // 非事务,需要考虑一致性问题
}

followers where subject = a order by ts desc offset x limit y;
fans where subject = a and ts < ? order by ts desc limit y;
followers where subject = a and status = 3 order by ts desc offset x limit y;
followers where subject = a  and object in (b, c, d);
fans where subject = a  and object in (b, c, d);

// 业务侧合并

补充说明

方案适用于千万用户量级,关注数据量级10亿左右的社区业务系统。

用户关系系统在社区业务中,读写都不高,百万日活的情况下,读 qps 不会超过三位数, 写就更少了。

方案涉及到分表,因此业务修改部分存在跨库事务的可能,此处建议追求最终一致性,业务侧双写,异步补偿。

方案之外

基于 Redis Sorted Set 的方案

有部分采用 Redis 实现用户关系的方案,此方案在用户量少时比基于数据库的成本高,在用户量大时不可用,完全不建议使用完全基于 Redis 的方案。

Redis 在关系业务中的可用场景,大 V 的粉丝列表和关注列表的前 N 页的缓存。

问题

数据倾斜,大 V 问题,部分用户可能有特别多的粉丝

针对部分用户维护单独的路由信息,写入和查询粉丝表时提前查询路由,根据路由操作相应的数据。

routes

idtablets
1rsdb.t1231655884150592

如果用户关系,只需要关注和好友,不考虑被关注的场景,实现是否会精简

可以少维护一个状态,或者查询关系时少一次查询。

REf