Build a feed stream system for tens of millions of users
Build a feed stream system that supports tens of millions of TPS/QPS based on the Tablestore Timeline model. This system handles user following, message publishing, and feed stream reading.
Solution overview
Feed streams are common in social media and content recommendation. After a user publishes a message, the system distributes the message to all followers and displays it in their feed streams in chronological order. Key challenges include high-concurrency message writes and reads (read-to-write ratio typically exceeds 10:1), dynamic user relationships, and guaranteed message delivery.
The Tablestore Timeline model separates messages into a store (outbox) and a sync (inbox), and synchronizes them with a combined push-pull approach. When a message is published, the system pushes it to all followers' inboxes (push mode). When reading a feed stream, the system scans the inbox sequentially by auto-increment sequence ID (pull mode). With auto-increment columns and search indexes, this architecture supports tens of millions of TPS/QPS.
Solution design
This solution uses five data tables:
|
Table |
Primary key |
Description |
|
feed_user_table |
user_id (String) |
User information table. Stores user attributes such as username, email, and gender. |
|
feed_user_relation_table |
main_user + sub_user (String) |
User relationship table. Records follow relationships. main_user is the follower, and sub_user is the followed user. |
|
feed_timeline_store_table |
timeline_id + sequence_id (auto-increment) |
Message store (outbox). Stores all messages published by users. Includes a search index that supports queries by sender, time, and content. |
|
feed_timeline_sync_table |
timeline_id + sequence_id (auto-increment) |
Message sync (inbox). Stores message copies pushed to followers. Feed stream reads scan from this table. |
|
feed_timeline_meta_table |
timeline_id (String) |
Timeline metadata table. Stores timeline type and associated user information. Includes a search index. |
Quick start
Try the feed stream system with either of the following methods:
ROS one-click deployment: Open the one-click deployment template and follow the on-screen instructions to complete the deployment. After deployment, go to the Outputs tab of the stack to get the Swagger API documentation URL. From there, test user registration, following, feed publishing, and feed stream reading online.
Run the sample code locally: Download the sample code feed.demo.zip (a Spring Boot project). Configure the instance information in
application.yml, and set access credentials in theOTS_AK_ENVandOTS_SK_ENVenvironment variables. Tables are created automatically on the first run.
The sample code provides the following REST APIs:
|
Method |
Path |
Description |
|
POST |
/users/register |
Register a user. |
|
POST |
/users/follow |
Follow a user. |
|
POST |
/users/unfollow |
Unfollow a user. |
|
POST |
/feeds/post |
Publish a message. |
|
GET |
/feeds/get |
Read the subscribed feed stream. |
|
GET |
/feeds/getUserPosts |
View messages published by a specific user. |
Implementation
If you already have an instance, skip this step. Otherwise, activate Tablestore and create an instance before you begin.
The following examples use the Timeline model library in Tablestore SDK for Java. Before you begin, install and configure Tablestore SDK for Java.
Step 1: Initialize the Timeline model
Use the Timeline SDK to initialize the store (outbox), sync (inbox), and meta store separately. Configure a search index for the store to support queries by sender and message content.
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();
Step 2: Follow a user
Call the PutRow operation of the base SDK to write a record to the user relationship table and establish a follow relationship.
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));
}
Step 3: Publish a message
To publish a message, write it to the sender's store (outbox) first, then push it to all followers' inboxes (sync). This is the push operation in the combined push-pull model.
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());
}
}
Step 4: Read the feed stream
To read the feed stream, scan messages from the sync library (inbox) sequentially by auto-increment sequence ID. This is the pull operation in the combined push-pull model. The client records the lastSequenceId from the previous read and resumes scanning from that position in the next request.
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;
}
To view all messages published by a specific user (user profile page), scan from the store instead of the sync library. The code structure is the same.
Resource cleanup
Delete the following resources when you no longer need them to avoid unnecessary charges.
Clean up resources in the following order:
Use the Timeline SDK to delete the store, sync, and meta store (including tables and indexes).
Delete the user table and relationship table.
(Optional) If you created a new instance for this solution, release the instance.
If you used the ROS one-click deployment, delete the stack in the ROS console to automatically clean up all resources.
// 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"));