数据库真的不太好学23333,很多都要自己上网搜素。
一、理论层面
朋友圈表结构
涉及朋友圈数据的有四个核心的表:
一个是发布。发布数据记录了来自所有用户所有的feed,比如一个用户发布了几张图片,每张图片的URL是什么,在CDN里的URL是什么,它有哪些元属性,谁可以看,谁不可以看等等。
一个是相册。相册是每个用户独立的,记录了该用户所发布的所有内容。
一个是评论。评论就是针对某个具体发布的朋友评论和点赞操作。
一个是时间线。所谓“刷朋友圈”,就是刷时间线,就是一个用户所有朋友的发布内容。
朋友圈架构
整体架构如下,
-
最上面为接入层,接入主要维护长连接,长连接主要为了安卓系统,一方面能够减少新建连接的性能损耗,另一方面由于谷歌的国内服务基本不可用,安卓的推送通知都是通过长连接哎完成的。
-
接入层后面是逻辑层,逻辑层不仅有朋友圈,也有iOS的系统的通知,因为iOS App进入后台后只有15s的存活期,所以iOS上的推送通知要用API的Push完成。
-
接下来是存储代理层,这一层主要负责一些关键数据的维护操作,比如用户在账号里面的动作操作和事故信息。
-
再往下是KV存储层,这里不存在业务逻辑,只是单纯的Key-Value映射,负载均衡和容错。
朋友圈流程举例
两个用户小王和Mary(如下图)。小王和Mary各自有各自的相册,可能在同一台服务器上,也可能在不同的服务器上。现在小王上传了一张图片到自己的朋友圈。上传图片不经过微信后台服务器,而是直接上传到最近的腾讯CDN节点,速度非常快。图片上传到该CDN后,小王的微信客户端会通知微信的朋友圈CDN:这里有一个新的发布(比如叫K2),这个发布的图片URL是什么,谁能看到这些图片,等等此类的元数据,来把这个发布写到发布的表里。
在发布的表写完之后,会把这个K2的发布索引到小王的相册表里。所以相册表其实是很小的,里面只有索引指针。相册表写好了之后,会触发一个批处理的动作。这个动作就是去跟小王的每个好友说,小王有一个新的发布,请把这个发布插入到每个好友的时间线里面去。
救命:什么是索引指针,什么是批处理
Mary上朋友圈了,而Mary是小王的一个好友。Mary拉自己的时间线的时候,时间线会告诉到有一个新的发布K2,然后Mary的微信客户端就会去根据K2的元数据去获取图片在CDN上的URL,把图片拉到本地。在这个过程中,发布是很重的,因为一方面要写一个自己的数据副本,然后还要把这个副本的指针插到所有好友的时间线里面去。如果一个用户有几百个好友的话,这个过程会比较慢一些。这是一个单数据副本写扩散的过程。但是相对应的,读取就很简单了,每一个用户只需要读取自己的时间线表,就这一个动作就行,而不需要去遍历所有好友的相册表。
使用写扩散的原因是,如果使用读是很容易失败的,一个用户如果要去读两百个好友的相册表,极端情况下可能要去两百个服务器上去问,这个失败的可能性是很大的。但是写失败了就没关系,因为写是可以等待的,写失败了就重新去拷贝,直到插入成功为止。
至于赞和评论的实现,是相对简单的。上面说了微信后台有一个专门的表存储评论和赞的数据,比如Kate是Mary和小王的朋友的话,刷到了K2这一条发布,就会同时从评论表里面拉取对应K2的、Mary留下的评论内容,插入到K2内容的下方。而如果另一个人不是Mary和小王的共同朋友,则不会看到这条评论。
救命:什么是写扩列?
写扩散是主动把消息写到订阅者的消息列表里,这样订阅者就不用去我的outbox拉取消息 ,所以当我要是有很多订阅者时,我就要写很多次,这就是上面定义中说的写很重
仅好友可见功能
调用判断:当需要判断好友01id的界面能否取得某具体内容数据的时候,只需要通过表2判断该内容所归属usrid的好友关系列表表1中有没有 好友01id。
网上的一些trick
1.首先以每个用户的id为key生成一个list,list最好根据需求限制一下长度,毕竟不会有人刷朋友圈的时候会刷到前面几千条数据去吧
2.然后当用户A发布内容的时候往关注A的用户的list里将内容lpush进去(因为关注人可能比较多,可以使用异步操作),用户A删除内容的时候也将关注人list里相对的内容删除
3.当用户要查看朋友圈的时候就返回redis里的list的数据就行(也支持分页)
4.当用户关注或者取消关注一个人的时候需要清空list然后在关系数据库中搜索所有关注人发布的内容并存到list里面
二、应用层面
代码实现
- 消息表很好理解,存储所有用户发送的所有内容,图片存地址。
utf8mb4格式可以存储emoji表情,具体可以参照之前的一篇文章
#消息表
CREATE TABLE friend_circle_message (
id bigint(15) NOT NULL AUTO_INCREMENT COMMENT '主键',
uid bigint(15) DEFAULT NULL COMMENT '用户id',
content varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
picture varchar(200) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT '' COMMENT '图片',
location varbinary(100) DEFAULT '' COMMENT '位置',
create_time datetime DEFAULT NULL COMMENT '创建日期',
PRIMARY KEY (id)
FOREIGN KEY(uid) REFERENCES friend_circle_user(id)
- uid表示作者id,可根据这个uid查询friend_circle_user表知道这个作者是谁。
- 时间轴表在朋友圈中是最关键的,存储着所有用的时间轴信息,因为当用户去拉取好友圈的时候,查询的就是本表,is_own字段用来区分当前数据是自己的发布还是好友发布的消息。
#时间轴表
CREATE TABLE friend_circle_timeline (
id bigint(15) NOT NULL AUTO_INCREMENT,
uid bigint(15) DEFAULT NULL COMMENT '用户id',
fcmid bigint(15) DEFAULT NULL COMMENT '朋友圈信息id',
is_own int(1) DEFAULT '0' COMMENT '是否是自己的',
create_time datetime DEFAULT NULL COMMENT '创建日期',
PRIMARY KEY (id)
)
- 评论表,每个元组就是一个评论;关联着一个用户、一个动态
#评论表
CREATE TABLE friend_circle_comment (
id bigint(15) NOT NULL AUTO_INCREMENT COMMENT'评论编号',
fcmid bigint(15) DEFAULT NULL COMMENT '朋友圈信息id',
review_to bigint(15) DEFAULT NULL COMMENT '回复对象ID',
uid bigint(15) DEFAULT NULL COMMENT '评论者id',
content varchar(500) DEFAULT NULL,
create_time datetime DEFAULT NULL COMMENT '创建日期',
like_count int(10) DEFAULT '0' COMMENT '点赞数',
PRIMARY KEY (id)
)
4.用户列表,存储所有用户的信息
#用户列表
CREATE TABLE friend_circle_user (
id bigint(15) NOT NULL AUTO_INCREMENT COMMENT '主键',
uid bigint(15) DEFAULT NULL COMMENT '用户id主键',
nickname varchar(500) COMMENT '昵称比如蛋卷超人',
sex int(1) COMMENT '性别',
password varchar(500) COMMENT '登陆密码',
PRIMARY KEY (id)
)
好友圈逻辑
- 发布朋友圈消息
当用户发布一条朋友圈消息的时候,后端逻辑的处理(A和B已经是好友关系):
用户A在朋友圈中发布一条消息,消息表t_friend_circle_message写入一条数据。时间轴表t_friend_circle_timeline中增加一条数据,uid设置A,is_own设置为1,表示在A的时间轴中增加一条自己发布的消息。查询用户A的好友,查到用户B(如果有还有其他好友D、E等等同样处理)时间轴表t_friend_circle_timeline中增加一条数据,uid设置B,is_own设置为0,表示在B的时间轴中增加一条好友发布的消息。 - 添加好友
当用户A,添加用户C为好友之后,触发同步好友时间轴的操作
INSERT INTO t_friend_circle_timeline (uid,fcmid,is_own,create_time)
SELECT #{uid},`id`,0,create_time FROM t_friend_circle_message WHERE uid = #{fid};
把消息表t_friend_circle_message好友C发布的所有消息添加到自己的时间轴中。
再把消息表t_friend_circle_message自己发布的消息添加到好友C的时间轴中。
使用好is_own字段,因为都是互相添加好友的消息到自己的时间轴中,所以都应该为false(0)。
点赞实现
点赞其实很好做,记录点赞数++ 就可以实现,但是我们需要判断出当前用户是否点赞过,点过赞的标识出已点赞的状态,所以我们需要记录一条消息的点赞人id,当用户每次点赞的时候去查询一下点赞列表里是否存在当前用户的id。
消息id作为key,点赞人的uid作为value,放到redis中。
(存储的时候没有使用数组或字符串,而是直接把list[long] 存储的uid集合序列化了。在读取遍历的时候比较方便,但是取消点赞的时候需要遍历移除掉其中一位,不确定list合适不合适做为存储结构。)
@Override
public Page<TimelineDetail> page(long uid, int page, int pageSize) {
int startNumber = (page - 1) * 10;
Collection<TimelineDetail> list = timelineDetailMapper.page(uid, startNumber, pageSize);
list.forEach(i -> getLikedAndCount(i, uid));
return new Page<TimelineDetail>(list, 0, pageSize, page);
}
/**
* 拿到是否点过赞 和点赞总数
* 再获取点赞的人名。。
*/
private void getLikedAndCount(TimelineDetail timelineDetail, long uid) {
Collection<Long> list = getLikeList(timelineDetail.getMessageId());
if (CollectionUtils.isNotEmpty(list)) {
List<String> nicknames = timelineDetailMapper.listNickname(list);
if (CollectionUtils.isNotEmpty(nicknames)) {
StringBuilder sb = new StringBuilder();
nicknames.stream().filter(StringUtils::isNotEmpty).forEach(i -> sb.append(i).append(","));
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
timelineDetail.setLikeNickname(sb.toString());
}
list.stream()
.filter(i -> i == uid)
.forEach(i -> timelineDetail.setLiked(true));
timelineDetail.setLikeCount(list.size());
}
}
查询朋友圈的时候需要遍历redis中的值,然后把uid替换成昵称。