基于表格存储 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_ENV和OTS_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"));