背景
我们来聊一个解决方案:我们做了一个和抖音产品类似的图文社区,社区有一个搜索栏,通过名字搜索用户,搜索出来的用户需要体现出其与当前用户的关系:1.当前用户的粉丝。2.当前用户关注的人。3.互相关注。目前总用户量在两百万左右,所以,目前搜索还是用的mysql,使用的like查询,这一块不是重点讨论。重点讨论的是,如何设计用户和关注用户关系的表,使得我们在查询时能直接查询出对应的关系并排序的逻辑。
方案一
1. 用户表(user_table)
主要用来存储社区中所有用户的基本信息,结构大致如下:
字段名 | 类型 | 说明 |
---|---|---|
user_id | int(或其他合适的整型) | 用户的唯一标识 ID |
username | varchar | 用户的名字(用于搜索栏搜索的字段等) |
other_fields | ... | 诸如性别、年龄等其他用户相关的拓展字段 |
2. 关注关系表(follow_relationship_table)
用于记录用户之间的关注关系,结构可以这样设计:
字段名 | 类型 | 说明 |
---|---|---|
follower_id | int | 关注者的用户 ID,也就是执行关注操作的那个用户的 ID |
followed_id | int | 被关注者的用户 ID,也就是被他人关注的那个用户的 ID |
follow_time | datetime | 关注操作发生的时间,可用于后续按关注先后顺序等排序,比如新关注的排在前面等情况 |
逻辑解释:
通过这样的表结构,当一个用户(user A)关注另一个用户(user B)时,就在该表中插入一条记录,记录中follower_id
为 user A 的user_id
,followed_id
为 user B 的user_id
,同时记录下关注时间。
查询逻辑
当用户A在在搜索栏,输入某个用户名,比如:“明”,返回前10条记录,包含用户A关注名字中包含“明”的用户,用户A的粉丝名字中包含“明”的用户以及用户A和名字中包含“明”的相互关注的用户。
查询的SQL。
-- 子查询1:获取用户A关注的名字中包含“明”的用户信息及对应关注时间,并标记为“已关注”关系
SELECT u.user_id,u.username,'已关注' AS relationship,fr.follow_time
FROM user_table u
JOIN follow_relationship_table fr ON u.user_id = fr.followed_id
WHERE fr.follower_id = current_user_idAND u.username LIKE '%明%'
UNION ALL
-- 子查询2:获取关注用户A且名字中包含“明”的用户(即用户A的粉丝)信息及对应关注时间,并标记为“粉丝”关系
SELECT u.user_id,u.username,'粉丝' AS relationship,fr.follow_time
FROM user_table u
JOIN follow_relationship_table fr ON u.user_id = fr.follower_id
WHERE fr.followed_id = current_user_idAND u.username LIKE '%明%'
UNION ALL
-- 子查询3:获取与用户A相互关注且名字中包含“明”的用户信息及对应关注时间,并标记为“互相关注”关系
SELECT u.user_id,u.username,'互相关注' AS relationship,fr.follow_time
FROM user_table u
JOIN follow_relationship_table fr ON (u.user_id = fr.followed_id AND fr.follower_id = current_user_id)OR (u.user_id = fr.follower_id AND fr.followed_id = current_user_id)
WHERE u.username LIKE '%明%'
ORDER BY follow_time DESC -- 根据关注时间倒序排序,最新关注的排在前面
LIMIT 10; -- 限制只返回前10条记录
如果是这种表的设计方案,查询的复杂度非常高,需要考虑三种查询,查询效率也不高。
方案二
1. 用户表(user_table)
结构保持和之前类似,用于存储社区中所有用户的基本信息:
字段名 | 类型 | 说明 |
---|---|---|
user_id | int(或其他合适的整型) | 用户的唯一标识 ID |
username | varchar | 用户的名字(用于搜索栏搜索的字段等) |
other_fields | ... | 诸如性别、年龄等其他用户相关的拓展字段 |
2. 关注关系汇总表(follow_relation_table)
新增这张表用于提前汇总每个用户与其他用户之间的关注关系情况,结构如下:
字段名 | 类型 | 说明 |
---|---|---|
user_id | int | 用户的唯一标识 ID,关联到 user_table 中的 user_id |
related_user_id | int | 与之存在关注关系的其他用户的 ID |
relationship_type | tinyint(可根据实际情况选择合适类型) | 用不同的数值来表示关系类型,例如 1 表示 “已关注”(当前用户关注别人),2 表示 “互相关注”,3 表示 “粉丝”(别人关注当前用户),方便后续查询判断和筛选 |
follow_time | datetime | 关注操作发生的时间,可用于排序依据等 |
数据维护逻辑
情况一:用户 A 关注用户 B(单向关注)
当发生用户 A 关注用户 B 这样的操作时,需要往 follow_relation_table
中插入两条记录,来分别体现从不同角度的关注关系:
-
第一条记录(体现用户 A 对用户 B 的关注):
user_id
字段值设置为用户 A 的user_id
,这表示是从用户 A 的视角出发,记录其关注行为。related_user_id
字段值设置为用户 B 的user_id
,明确其关注的对象是用户 B。relationship_type
字段值设置为1
,根据预先定义的规则,1
代表 “已关注”,也就是当前用户(此处为用户 A)关注了别的用户(用户 B)。follow_time
字段则记录下此次关注操作发生的具体时间,可以通过获取系统当前时间来赋值,例如在很多编程语言中结合数据库操作库可以使用类似datetime.now()
(不同语言具体函数名和用法有差异)这样的方式获取当前时间并插入到该字段。
-
第二条记录(体现用户 B 相对用户 A 的粉丝关系):
user_id
字段值设置为用户 B 的user_id
,从用户 B 的角度来看待这个关注关系,即别人(用户 A)关注了自己。related_user_id
字段值设置为用户 A 的user_id
,表示与自己产生关联的这个 “别人” 是用户 A。relationship_type
字段值设置为3
,按照设定,3
代表 “粉丝”,意味着用户 A 是用户 B 的粉丝(因为 A 关注了 B)。- 同样,
follow_time
字段也记录此次关注操作发生的时间,与第一条记录的时间保持一致,因为这是同一个关注行为从不同主体角度的记录。
情况二:用户 A 关注用户 C,并且用户 C 关注用户 A(双向关注,即互相关注)
当出现这种互相关注的情况时,同样需要往 follow_relation_table
中插入两条记录来体现这种对称的关系:
-
第一条记录(体现用户 A 与用户 C 的互相关注关系从用户 A 角度):
user_id
字段值设置为用户 A 的user_id
,代表是从用户 A 这边出发来看待这个关系。related_user_id
字段值设置为用户 C 的user_id
,明确与之相关的是用户 C。relationship_type
字段值设置为2
,因为根据定义,2
表示 “互相关注”,这里用户 A 和用户 C 互相关注了对方。follow_time
字段记录此次关注操作发生的时间,获取方式同前面情况一的时间获取逻辑。
-
第二条记录(体现用户 A 与用户 C 的互相关注关系从用户 C 角度):
user_id
字段值设置为用户 C 的user_id
,从用户 C 的视角来记录这个关系。related_user_id
字段值设置为用户 A 的user_id
,表明与之相关的另一方是用户 A。relationship_type
字段值同样设置为2
,代表 “互相关注”,与第一条记录保持一致,因为这是双向的互相关注关系。follow_time
字段也记录此次关注操作对应的时间,和第一条记录的时间相同,毕竟是同一个互相关注行为在不同主体角度的体现。
另外,需要注意的是,在实际应用中,往往需要先判断是否已经存在相关的关注关系记录,以避免重复插入等情况导致数据异常。比如在执行用户 A 关注用户 B 的操作前,可以先查询 follow_relation_table
看是否已经存在对应的记录,如果不存在再执行上述插入逻辑;对于互相关注情况,判断会更复杂些,可能需要从两个方向去检查是否已有单向关注记录等,具体的判断逻辑也可以通过 SQL 查询语句结合业务代码来实现,这里为了聚焦数据写入逻辑暂未详细展开判断部分内容。
同时,随着业务的发展,可能还会涉及到取消关注等操作,那时就需要相应地去更新或者删除 follow_relation_table
中的相关记录,以保证数据的准确性和一致性,这也是后续在数据维护方面需要进一步考虑的问题。
查询逻辑
当用户A在在搜索栏,输入某个用户名,比如:“明”,返回前10条记录,包含用户A关注名字中包含“明”的用户,用户A的粉丝名字中包含“明”的用户以及用户A和名字中包含“明”的相互关注的用户。
查询的SQL。
SELECT ut.username,fr.relationship_type,fr.follow_time
FROM user_table ut
JOIN follow_relation_table fr ON ut.user_id = fr.related_user_id
WHERE fr.user_id = current_user_idAND ut.username LIKE '%明%'
ORDER BY fr.follow_time DESC
LIMIT 10;
这种方案,写入带来了极大麻烦,并且数据是双份写入。但是,查询却带来极大的便利。但是,对于读多写少的场景,我们认为这种方案是可取的。
总结
本质上第二种方案也是一种空间换时间的方案,把复杂的查询退化成简单的查询。空间换时间是一种思维方式,不能仅仅狭隘的认为用缓存换时间是空间换时间,同一个表基于不同的分表键或者分库健进行分表分库是空间换时间。