../rs
用户关系系统设计方案
Published:
用户关系系统设计方案
用户间存在关注,被关注,好友(互相关注)三种关系。
背景说明
名词解释
- 关注列表:指定用户关注的用户的列表
- 粉丝列表:关注指定用户的用户的列表
- 好友列表:与指定用户互相关注的用户的列表
- 用户关系状态:关注,被关注,好友,无关系。
关联数据
- 有关注行为的用户占比 40%, 这些用户平均关注 7.9 个用户。
- 被关注的用户占比约 35%,这些用户平均拥有 9.2 个粉丝。
- 既有关注行为也有粉丝的用户约 26%,这些用户平均关注 8.3 个用户,平均拥有 10.4 个粉丝。
关系枚举表
下文以 ->
表示关注,<-
表示被关注, <->
表示互相关注,--
表示无关系
A B | B A | |
---|---|---|
A -- B | -- | -- |
A -> B | -> | <- |
B -> A | <- | -> |
A -> B, B -> A | <-> | <-> |
业务场景
- 关注,用户 A 关注用户 B。
- 取关,用户 A 取消关注 用户 B。
- 获取用户 A 的关注列表,并获取用户 C 与该关注列表的关系状态。
- 获取用户 A 的粉丝列表,并获取用户 C 与该粉丝列表的关系状态。
- 获取用户 A 的好友列表。
业务约束
- 用户关注上限 1000,即用户最多关注 1000 个其他用户。
技术方案
基于不同的用户量级设计相关的方案, 百万级,千万级,亿级,十亿级。 方案里涉及数据删除均为逻辑删除。
单表方案
关注产生正反两条记录, 数据量为关注行为的两倍。
数据表设计
关注表 followers
- id, 数据 ID, 无意义
- subject 主语 uid
- object 宾语 uid
- ts 记录发生或修改时间
- status 状态,0 无, 1 被关注,2 关注,3 好友
id | subject | object | ts | status |
---|---|---|---|---|
1 | 10 | 20 | 1655884150592 | 2 |
1 | 20 | 10 | 1655884150592 | 1 |
2 | 10 | 30 | 1655884250592 | 3 |
3 | 30 | 10 | 1655884350592 | 3 |
索引
- unique index so(subject , object)
- index subject_ts (subject, ts desc)
业务操作
- 关注操作,用户 a 关注 用户 b
// 查询当前用户关系
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
// 查询用户 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
- id 数据标志,无意义
- subject 主语
- object 宾语
- ts 关注时间
id | subject | object | ts |
---|---|---|---|
1 | 10 | 20 | 1655884150592 |
1 | 20 | 10 | 1655884150592 |
1 | 30 | 40 | 1655884150592 |
索引
- unique index (subject,object)
- index subject_ts(subject, ts desc)
- index object_ts(object, ts desc)
好友表
- id 数据标志,无意义
- subject 主语
- object 宾语
- ts 时间
id | subject | object | ts |
---|---|---|---|
1 | 10 | 20 | 1655884150592 |
索引
- index (ts desc, subject, object)
业务操作
- 关注, A 关注 B
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, 可保证维护好友关系只需要一条记录
}
- 取关,A 取关 B
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
一次关注产生一条关注记录
- id 数据标志,无意义
- subject 主语
- object 宾语
- ts 关注时间
- status 0 无, 2 关注,3 好友
id | subject | object | ts | status |
---|---|---|---|---|
1 | 10 | 20 | 1655884150592 | 2 |
2 | 100 | 200 | 1655884150592 | 2 |
索引
- unique index so(subject, object)
- index s_ts(subject, ts desc)
粉丝表 fans
- id 数据标志,无意义
- subject 主语
- object 宾语
- ts 关注时间
id | subject | object | ts |
---|---|---|---|
1 | 20 | 10 | 1655884150592 |
2 | 200 | 100 | 1655884150592 |
粉丝表是基于关注表的维度表,数据一致性细节此处不展开。
业务操作
- 关注
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 不会超过三位数, 写就更少了。
方案涉及到分表,因此业务修改部分存在跨库事务的可能,此处建议追求最终一致性,业务侧双写,异步补偿。
- 基于 followers binlog 异步补偿
- 业务侧生产消息,消费者异步补偿
- 增量轮询 followers 表,异步补偿。
- 分布式事务,不推荐。
方案之外
基于 Redis Sorted Set
的方案
有部分采用 Redis
实现用户关系的方案,此方案在用户量少时比基于数据库的成本高,在用户量大时不可用,完全不建议使用完全基于 Redis
的方案。
Redis
在关系业务中的可用场景,大 V 的粉丝列表和关注列表的前 N 页的缓存。
问题
数据倾斜,大 V 问题,部分用户可能有特别多的粉丝
针对部分用户维护单独的路由信息,写入和查询粉丝表时提前查询路由,根据路由操作相应的数据。
routes
- id 用户ID
- table 数据表
- ts 加入新表时的时间
id | table | ts |
---|---|---|
1 | rsdb.t123 | 1655884150592 |
- 写,根据记录的 ts 判定去公用表还是特例表
- 读,ts 是必传参数,默认是当前时间,根据 ts 确定当前查询去公用表还是特例表
如果用户关系,只需要关注和好友,不考虑被关注的场景,实现是否会精简
可以少维护一个状态,或者查询关系时少一次查询。