如何打造一个轻量级的社交系统

在这里插入图片描述

简介

随着国外Facebook、Twitter、国内的微博等社交网络网站的崛起,很多公司也推出了类似的社交服务产品,相比与微博这种大型用户社交产品而言,很多公司包括育学园推出的类微博Feed流的社交产品,由于用户基数、用户活跃度等原因远没有微博庞大,因此这些产品在数据存储、Feed展示上的技术实现远没有微博的复杂,育学园的用户量级在1000万左右,旧社交系统中单表已有存量数据为2000多万,面对具有实时特性的Feed流,我们如何去打造一个轻量级的社交系统呢?

背景

因技术架构、产品内容升级,原有的社交系统已无法满足新业务,因此重构了一套新的社交系统,并将旧系统的数据迁移到新系统中并完成产品内容迭代。

Feed流相关概念

  • Feed

Feed流中的每一条状态或者消息都是Feed,比如朋友圈中的一个状态就是一个Feed,微博中的一条微博就是一个Feed。

  • Feed流

持续更新并呈现给用户内容的信息流。每个人的朋友圈,微博关注页等等都是一个Feed流。

  • Timeline

Timeline其实是一种Feed流的类型,微博,朋友圈都是Timeline类型的Feed流,但是由于Timeline类型出现最早,使用最广泛,最为人熟知,有时候也用Timeline来表示Feed流。

  • 关注页Timeline(收件箱)

展示已关注用户Feed消息的页面,比如朋友圈,微博的首页等

  • 个人页Timeline(发件箱)
    展示自己发送过的Feed消息的页面,比如微信中的相册,微博的个人页等

  • 感兴趣的人

二度好友,我关注的人的好友,我好友的好友,我关注人的关注,我好友的关注

Feed流实现的几种方案

  • 拉模式

方式:
发布Feed时向个人页Timeline写入feedId,读取Feed流时先获取所有的关注列表,在获取每一个关注用户的个人页Timeline,排序后展示。
优点:
写入简单
缺点:
读取复杂
适用场景:
少量用户

  • 推模式

方式:
发布Feed时向所有关注者关注Timeline广播写入feedId,并写入个人页Timeline,读取关注页Feed流时从关注页Timeline读取,读取个人页Feed流时从个人页Timeline读取。
优点:
读取简单
缺点:
读取膨胀
适用场景:
关注数相对平均

  • 推拉结合

方式一 (大V模式):
发布Feed时先写入个人页Timeline,然后判断自己是否是大V用户,如果不是就采用推模式,如果是就结束
读取Feed时先从自己的关注页Timeline读取,然后读取自己关注的大V用户的个人页Timeline,最后合并按照时间排序展示
方式二(活跃模式)
**在线推 **:向所有在线关注者关注Timeline广播写入feedId,并写入个人页Timeline
离线拉 :在APP启动时启用后台线程根据个人页Timeline最后一个Feed时间去所有关注者拉取新Feed并写入到关注页Timeline
优点:
可实现大V用户场景,活跃用户能最快看到最新信息

缺点:
实现复杂
适用场景:
有大V用户场景

确定Feed流实现方案

旧社交系统中粉丝数量Top 100的用户

序号被关注人数
131391
228646
320749
420630
519292
619131
…………
98972
99966
100966

结合业务实际的数据量级,我们采用成本相对较低的推模式来实现Feed流,标题中的所谓“轻量”正是指的我们这里没有大V用户,不用去考虑非常复杂的推拉结合的实现模式。

Feed流推模式

  • 发布Feed时向所有关注者关注Timeline广播写入feedId,并写入个人页Timeline
  • 读取关注页Feed流时从关注页Timeline读取,读取个人页Feed流时从个人页Timeline读取

推模式下的核心流程

关注用户

在这里插入图片描述

取消关注用户

在这里插入图片描述

发布Feed

在这里插入图片描述

删除Feed

在这里插入图片描述

数据库结构

数据存储采用Mysql

Table:fans_list
Desc:粉丝列表,存储所有的粉丝列表

CREATE TABLE `fans_list` (
  `id` bigint(20) NOT NULL,
  `member_id` varchar(20) NOT NULL COMMENT '用户ID',
  `fans_member_id` varchar(20) NOT NULL COMMENT '粉丝用户ID',
  `follower_at` bigint(19) DEFAULT NULL COMMENT '关注时间',
   PRIMARY KEY (`id`),
  KEY `idx_member_id` (`member_id`),
  KEY `idx_fans_member_id` (`fans_member_id`)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Table:follower_list
Desc:关注列表,存储所有的关注列表

CREATE TABLE `follower_list` (
  `id` bigint(20) NOT NULL,
  `member_id` varchar(20) NOT NULL COMMENT '用户ID',
  `follower_member_id` varchar(20) NOT NULL COMMENT '关注用户ID',
  `follower_at` bigint(19) DEFAULT NULL COMMENT '关注时间',
  PRIMARY KEY (`id`),
  KEY `idx_member_id` (`member_id`),
  KEY `idx_follower_member_id` (`follower_member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Table:follower_timeline
Desc:关注页timeline (收件箱) ,存储所有关注用户发送的FeedId

CREATE TABLE `follower_timeline` (
  `id` bigint(20) NOT NULL,
  `member_id` varchar(20) NOT NULL COMMENT '用户ID',
  `follower_member_id` varchar(20) NOT NULL COMMENT '被关注用户ID'
  `feed_id` varchar(32) NOT NULL COMMENT '发布的内容id',
  `publish_at` bigint(19) NOT NULL COMMENT '发布时间'
  PRIMARY KEY (`id`),
  KEY `idx_member_id` (`member_id`),
  KEY `idx_follower_member_id` (`follower_member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Table:personal_timeline
Desc:个人页timeline (发件箱) ,存储自己发送的FeedId

CREATE TABLE `personal_timeline` (
  `id` bigint(20) NOT NULL,
  `member_id` varchar(20) NOT NULL COMMENT '用户ID',
  `feed_id` varchar(32) NOT NULL COMMENT '发布的内容id',
  `publish_at` bigint(19) NOT NULL COMMENT '发布时间',
  PRIMARY KEY (`id`),
  KEY `idx_member_id` (`member_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

目前旧系统存储的 关注页timeline (收件箱) 单表数据量已经超过2000万,因此我们将在存储上做了分库分表设计,一共分了4个库,64张表,单表2000万的数据,按照比较理想的平均分配来算单表的数据量会在32万左右,这样在查询的上短期内不会有瓶颈了。
分库分表采用了比较轻的基于客服端实现的Sharding-JDBC框架。

  • Sharding-JDBC足够轻,使用成本低
  • 存储层未来可能会切换到TiDB,TiDB原生支持能够将已分库分表数据的导入进去

在这里插入图片描述

Feed聚合

由于收件箱、发件箱在数据存储上只存储了用户ID、FeedID等基本的索引信息,而Feed在实际显示中会显示很丰富的内容,比如:用户头像、昵称、Feed正文、标题、发布时间、点赞数量、评论数据、转发数量、关注状态、收藏状态等等一系列数据,仅凭已有的ID是不够的,因此在查询到FeedID后,还需要聚合Feed内容。
以查询一个用户的关注列表Feed流(查询收件箱)为例:

  • 根据用户ID查询follower_timeline表,得到用户ID、FeedID
  • 通过用户ID、FeedID查询用户系统、Feed系统、评论系统、计数系统、收藏系统 等聚合Feed内容数据

从查询FeedID列表到得到FeedContent列表的过程
在这里插入图片描述

一个真实的Feed Content列表数据如下:
在这里插入图片描述

可以发现当我们得到一个用户ID和FeedID后,仍需要去做大量的数据查询才能拼凑出来实际的Feed流内容,因为在微服务架构下,这些数据都分散在各个系统。

如何高效查询关注页、个人页的FeedID列表?

  • 因为Feed有个特点是它的时效性,一般很少有人去翻看上周、上个月的Feed,所以我们可以使用codis来缓存关注页、个人页最新的N条热数据,当查询的数据在N之内,则直接从codis中返回,当查询的数据在N之外,则查询DB

/**
上拉加载、下拉刷新相关伪代码
*/

//热数据最大存储数量,如果每次查询20条Feed,那么缓存中的500条热数据可以满足前25页的查询
int N = 500;
 
//上拉加载更多
//根据上一条观看的FeedId来分页查询
List<?> loadMore(String memberId, Long lastId, Integer pageSize){
    List<?> value = init(memberId);
    //通过lastId定位索引
    int idx = indexOf(value, memberId, lastId);
    int nextIdx = idx + 1;
    //1 超出热数据范围 走DB
    if(idx == -1 || nextIdx + pageSize > N){
        return findDb(memberId, lastId, pageSize);
    }
    //2 没有超出热数据范围 命中缓存
    return value.size() < nextIdx + pageSize ? value.subList(nextIdx, value.size()) : value.subList(nextIdx, nextIdx + pageSize)
}
//下拉刷新 获取最新的feed
List<?> reflush(String memberId, Integer pageSize) {
  
    //1 超出热数据范围 走DB
    if(pageSize > N) {
        return findDb(memberId, pageSize);
    }
 
    //2 没有超出热数据范围 命中缓存
    List<?> value = init(memberId);
    return value.size() < pageSize ? value : value.subList(0, pageSize);
}
public int indexOf(List<> list, Long lastId) {
    for(int i = 0; i < list.size(); i ++) {
        if(list.get(i).getId().longValue() == lastId.longValue()) {
            return i;
        }
    }
    return -1;
}
//从DB中初始化Feed到redis
List<?> init(String memberId) {
    lock(memberId.inter());
    String key = ...;
    //1 从redis中获取热数据
    long count = redis.zcard(key);
    //2 没有热数据
    if(count == 0) {
        List<?> value = findDb(memberId, N);
        //3 初始化热数据
        for(....)
            redis.zadd(key, score, v);
        return value;
    }
    return redis.zrevrange(key, 0, -1);
}

根据FeedID列表如何保证在预期时间内从超过10+个系统中聚合Feed内容?

  • 每个微服务都提供高效的批量查询RPC接口,如:根据用户ID列表批量获取多个用户信息、根据FeedID列表批量获取Feed点赞数量等,对每个接口的RT有要求
  • 使用线程池并行调用RPC接口获取数据,采用ThreadPoolExecutor.invokeAll方法批量执行Task,并设定总的超时时间,对所有线程总的执行时间有要求
//定义线程池 
ThreadPoolExecutor executor = new ThreadPoolExecutor(
                10,
                100,
                5l,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>());
//task的数量根据实际业务来定义
//task1 通过Rpc调用获取数据
Callable<Object> task1 = new Callable<Object>() {
    public Object call()  {
        try {
            //RPC call
        } finally {
        }
        return null;
    }
};
//task2 通过Rpc调用获取数据
Callable<Object> task2 = new Callable<Object>() {
    public Object call()  {
        try {
            //RPC call
        } finally {
        }
        return null;
    }
};
//task3 通过Rpc调用获取数据
Callable<Object> task3 = new Callable<Object>() {
    public Object call()  {
        try {
            //RPC call
        } finally {
        }
        return null;
    }
};
//执行所有任务,并设定总超时时间为5秒
executor.invokeAll(Arrays.asList(task1, task2, task3), 5l, TimeUnit.SECONDS);

如何确保在聚合Feed过程中不会因为其中某一个任务接口响应过慢而导致整个Feed数据不完整而影响展现?

  • 将拉取数据任务划分等级,分为必要数据非必要数据,必要数据如果缺失则整个Feed拉取失败,非必要数据如果缺失则采用降级容错策略填充数据,如:用户头像、昵称、Feed内容为必要数据, 点赞数量、评论数量、标签等数据为非必要数据,可根据实际业务采用默认值、空值填充策略

图为育学园线上业务最近一小时聚合Feed的RPC接口调用次数、响应时间相关数据,每个RPC接口在一次查询20条Feed的情况下,每个服务均部署了2台,系统调用链超过10+个服务,其整体聚合的时间在50ms上下,由此可见整个Feed聚合过程、多线程调用各个RPC链路的性能还是非常可观的

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OpenSNS一款有“身份”的开源免费SNS社交系统,包含 微博、资讯、活动、论坛、专辑、积分商城、群组、充值中心、 问答、分类信息、微店等模块,支持PC端、手机网页版、客户端、 微信版等多种平台,可以为客户快速搭建社交网站。2015年1月28日ThinkOX 正式更名为 OpenSNS。 OpenSNS v3.0 更新的具体内容: 【修改】后台采用基于Bootstrap 3.x的高级管理控制面板主题:AdminLTE,AdminLTE - 是一个完全响应式管理模板。基于Bootstrap3框架,界面简洁清爽,易于使用。 【增加】行为日志新增筛选和导出功能,可以根据行为和日期筛选自己想要的数据、导出功能也便于对数据进行处理和备份。 【增加】新增排行榜,分别是粉丝排行,积分排行,连签排行和累签排行。 【增加】新增签到日历,清晰跟踪记录自己的“足迹” 【增加】后台新增统计模块,包括网站统计,活跃用户统计,留存率统计,流失率统计和充值用户统计。 【增加】用户组新增有效期。有效期为选填项,选择后用户组将在到期后自动关闭。便于网站开展临时或短期活动时使用。 【调整】邀请机制优化,告别繁琐的操作,注册后用户即可获得专属自己的链接。 【调整】公告系统优化,新版公告系统解决了无法与主题很好兼容的问题,可选择是否强制推送,强制推送的公告将以弹窗的形式推送给用户,而没有选择强制推送的公告将以系统消息的形式推送给用户,可查看有多少人,哪些人查看了公告。 【调整】签到插件优化,现在签到可以绑定多事件 【调整】前台UI进行了大的调整,修改了配色和主题、登录注册和微博 【调整】消息系统全面升级,以会话的形式对消息进行分类,支持模板消息 【调整】优化身份创建,新增身份创建向导 【新增】新增全站搜素 【新增】管理员现在可以修改编辑网站用户的部分资料
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值