搭建千万级 Feed 流系统

更新时间:
复制 MD 格式

基于表格存储 Timeline 模型,搭建支持千万级 TPS/QPS 的 Feed 流系统,实现用户关注、消息发布和 Feed 流读取。

方案概述

Feed 流(信息流)广泛应用于社交媒体、资讯推荐等场景。用户发布消息后,系统通过用户关系将消息推送给所有关注者,并在其 Feed 流中按时间排序展示。核心挑战包括:高并发的消息写入与读取(读写比通常 10:1 以上)、动态变化的用户关系、消息必达性保障。

表格存储 Timeline 模型内置存储库(发件箱)和同步库(收件箱)双库架构,采用推拉结合的消息同步策略。发布消息时,系统将消息推送到所有关注者的收件箱(推模式);读取 Feed 流时,从收件箱按自增序列号顺序扫描(拉模式)。配合自增列和多元索引,可支持千万级 TPS/QPS。

方案设计

本方案使用 5 张数据表实现 Feed 流系统,各表职责如下。

表名

主键

说明

feed_user_table

user_id(String)

用户信息表,存储用户名、邮箱、性别等属性。

feed_user_relation_table

main_user + sub_user(String)

用户关系表,记录关注关系。main_user 为关注者,sub_user 为被关注者。

feed_timeline_store_table

timeline_id + sequence_id(自增)

消息存储库(发件箱),保存用户发布的所有消息。含多元索引,支持按发送者、时间、内容检索。

feed_timeline_sync_table

timeline_id + sequence_id(自增)

消息同步库(收件箱),保存推送给关注者的消息副本。读取 Feed 流时从此表扫描。

feed_timeline_meta_table

timeline_id(String)

Timeline 元数据表,存储 Timeline 类型和关联用户等信息。含多元索引。

快速体验

可以通过以下两种方式快速体验 Feed 流系统:

  • ROS 一键部署:打开一键配置模板链接,按页面提示完成部署。部署完成后,在资源栈输出页签获取 Swagger 接口文档地址,可在线测试用户注册、关注、发布 Feed 和读取 Feed 流。

  • 本地运行示例代码:下载示例代码feed.demo.zip(Spring Boot 项目),配置 application.yml 中的实例信息,通过环境变量 OTS_AK_ENVOTS_SK_ENV 设置访问密钥。首次运行时自动建表。

示例代码提供以下 REST API:

方法

路径

说明

POST

/users/register

注册用户

POST

/users/follow

关注用户

POST

/users/unfollow

取消关注

POST

/feeds/post

发布消息

GET

/feeds/get

读取订阅的 Feed 流

GET

/feeds/getUserPosts

查看指定用户发布的消息

方案实现

开始前,开通表格存储服务并创建实例(如已有可用实例,跳过此步骤)。

说明

以下示例使用 Java SDK 的 Timeline 模型库。开始前,安装和配置Java SDK

步骤一:初始化 Timeline 模型

通过 Timeline SDK 分别初始化存储库(Store,发件箱)、同步库(Sync,收件箱)和元数据库(Meta)。存储库配置多元索引,支持按发送者和消息内容检索。

TimelineStoreFactory serviceFactory = new TimelineStoreFactoryImpl(syncClient);

// Initialize store (outbox): stores all published messages
TimelineIdentifierSchema idSchema = new TimelineIdentifierSchema.Builder()
        .addStringField("timeline_id").build();

IndexSchema timelineIndex = new IndexSchema();
timelineIndex.addFieldSchema(new FieldSchema("sender", FieldType.KEYWORD)
        .setIndex(true).setEnableSortAndAgg(true).setStore(true));
timelineIndex.addFieldSchema(new FieldSchema("send_time", FieldType.LONG)
        .setIndex(true).setEnableSortAndAgg(true).setStore(true));
timelineIndex.addFieldSchema(new FieldSchema("text", FieldType.TEXT)
        .setIndex(true).setStore(true).setAnalyzer(FieldSchema.Analyzer.MaxWord));

TimelineSchema storeSchema = new TimelineSchema("feed_timeline_store_table", idSchema)
        .autoGenerateSeqId()
        .setTimeToLive(-1)
        .withIndex("feed_timeline_store_index", timelineIndex);

TimelineStore store = serviceFactory.createTimelineStore(storeSchema);

// Initialize sync (inbox): receives pushed messages from followed users
TimelineSchema syncSchema = new TimelineSchema("feed_timeline_sync_table", idSchema)
        .autoGenerateSeqId()
        .setTimeToLive(-1);

TimelineStore sync = serviceFactory.createTimelineStore(syncSchema);

// Initialize meta: stores timeline metadata
IndexSchema metaIndex = new IndexSchema();
metaIndex.setFieldSchemas(Arrays.asList(
        new FieldSchema("type", FieldType.KEYWORD)
                .setIndex(true).setEnableSortAndAgg(true).setStore(true),
        new FieldSchema("user", FieldType.TEXT)
                .setIndex(true).setStore(true).setAnalyzer(FieldSchema.Analyzer.MaxWord)
));

TimelineMetaSchema metaSchema = new TimelineMetaSchema("feed_timeline_meta_table", idSchema)
        .withIndex("feed_timeline_meta_index", metaIndex);

TimelineMetaStore metaStore = serviceFactory.createMetaStore(metaSchema);

// Create all tables (call once during initialization)
store.prepareTables();
sync.prepareTables();
metaStore.prepareTables();

步骤二:关注用户

调用基础 SDK 的 PutRow 接口写入用户关系表,建立关注关系。

public void follow(String userA, String userB) {
    PrimaryKey primaryKey = PrimaryKeyBuilder.createPrimaryKeyBuilder()
            .addPrimaryKeyColumn("main_user", PrimaryKeyValue.fromString(userA))
            .addPrimaryKeyColumn("sub_user", PrimaryKeyValue.fromString(userB))
            .build();

    RowPutChange rowPutChange = new RowPutChange("feed_user_relation_table", primaryKey);
    rowPutChange.addColumn("timestamp", ColumnValue.fromLong(System.currentTimeMillis()));

    syncClient.putRow(new PutRowRequest(rowPutChange));
}

步骤三:发布消息

发布消息时,先写入发送者的存储库(发件箱),再推送到所有关注者的同步库(收件箱)。这是推拉结合模式中的"推"操作。

public void sendMessage(String userId, Message message, List<String> followers) {
    // 1. Write to sender's store (outbox)
    TimelineIdentifier identifier = new TimelineIdentifier.Builder()
            .addField("timeline_id", message.getTimelineId())
            .build();
    store.createTimelineQueue(identifier).store(message.getTimelineMessage());

    // 2. Push to each follower's sync (inbox)
    for (String friend : followers) {
        TimelineIdentifier friendId = new TimelineIdentifier.Builder()
                .addField("timeline_id", friend)
                .build();
        sync.createTimelineQueue(friendId).store(message.getTimelineMessage());
    }
}

步骤四:读取 Feed 流

读取 Feed 流时,从同步库(收件箱)按自增序列号顺序扫描消息。这是推拉结合模式中的"拉"操作。客户端记录上次读取的 lastSequenceId,下次请求时从该位置继续扫描。

public List<Post> fetchFeed(String userId, long lastSequenceId) {
    TimelineIdentifier identifier = new TimelineIdentifier.Builder()
            .addField("timeline_id", userId)
            .build();

    ScanParameter parameter = new ScanParameter()
            .scanForward(lastSequenceId)
            .maxCount(30);

    Iterator<TimelineEntry> iterator = sync.createTimelineQueue(identifier).scan(parameter);

    List<Post> posts = new LinkedList<>();
    int counter = 0;
    while (iterator.hasNext() && counter++ <= 30) {
        TimelineEntry entry = iterator.next();
        TimelineMessage msg = entry.getMessage();

        Post post = new Post()
                .setName(msg.getString("sender"))
                .setTime(msg.getLong("send_time"))
                .setContent(msg.getString("text"))
                .setSequenceId(entry.getSequenceID());
        posts.add(post);
    }
    return posts;
}

查看指定用户发布的所有消息(用户首页)时,从存储库(Store)而非同步库(Sync)扫描即可,代码结构相同。

资源清理

说明

不再需要本方案中创建的资源时,清理以下资源以避免产生不必要的费用。

按以下顺序清理资源:先通过 Timeline SDK 删除存储库、同步库和元数据库(包含表和索引),再删除用户表和关系表。如果实例是为本方案新建的,最后释放实例。如果使用了 ROS 一键部署,在 ROS 控制台删除资源栈即可自动清理所有资源。

// Clean up Timeline tables and indexes
store.dropAllTables();
sync.dropAllTables();
metaStore.dropAllTables();

// Clean up user and relation tables
syncClient.deleteTable(new DeleteTableRequest("feed_user_table"));
syncClient.deleteTable(new DeleteTableRequest("feed_user_relation_table"));