Architectural Redesign of a Feed Stream System
A legacy feed stream feature within an educational mobile application suffered from severe performance degradation and code decay. The system managed dynamic content (images, videos, audio) to student-teacher interactions within class groups. The existing monolithic service, built on MySQL with over 20 million records across core tables, relied heavily on stored procedures, leading to frequent query timeouts exceeding 5 seconds.
Refactoring Objectives
- Decompose the monolith into maintainable modules.
- Support flexible addition of new feed types.
- Optimize the response time for aggregated feed pages.
Architectural Changes
Service Decomposition
The original service was split into:
- API Service: Exposes RESTful endpoints for client applications.
- Task Service: Handles asynchronous tasks like notifications.
Database Strategy
Sharding Design
Given the write volume of approximately 20,000 entries per day, table partitioning was chosen over full sharding.
Primary Key Generation: A variant of the Snowflake algorithm was implemented.
- 41-bit timestamp (millisecond precision)
- 10-bit worker ID derived from
crc32(sharding_key) % 1024 - 12-bit sequence number, managed via Redis
INCRBYwith a configurable step. - Local caching was added to reduce Redis calls.
// Pseudocode for ID generation
long shardKeyHash = crc32(shardingKeyValue);
int workerNodeId = Math.abs(shardKeyHash % 1024);
String redisCounterKey = "id_gen:" + currentMillis;
long sequence = redisClient.increment(redisCounterKey, batchSize);
long uniqueId = combine(currentMillis, workerNodeId, sequence);
Routing Logic: Queries using the sharding key (e.g., user ID) are routed directly. For non-sharding key queries (e.g., by primary key), the worker ID embedded in the Snowflake ID is used to determine the partition.
Library Selection: sharding-jdbc was adopted for its lightweight client-side approach.
// Simplified sharding-jdbc setup
Map<String, DataSource> dataSourceMap = createDataSources();
ShardingRuleConfiguration ruleConfig = new ShardingRuleConfiguration();
ruleConfig.getTableRuleConfigs().add(createTableRule("feed_table", "feed_table_${0..3}"));
DataSource shardedDataSource = new ShardingDataSource(dataSourceMap, ruleConfig, props);
Query Optimization
Caching Strategy for Lists: Two patterns were considered.
- Cache entire pages: Simple but inefficient for frequently updated content.
- Cache individual items: Fetch IDs first, then retrieve items from cache, falling back to the database for misses. This was selected to its efficiency.
Feed Aggregation: Data for a feed (content, likes, comments) is cached in Redis.
- Content: Stored as a Hash.
- Likes: Stored as a ZSET (sorted by timestamp) of user IDs.
- Comments: Stored as a ZSET of comment IDs, with full comment data in Strings.
- Collections: Stored as Strings mapping user IDs to feed IDs.
A Redis pipeline is used to atomically update related data structures.
// Pseudocode for Redis pipeline
try (RedisPipeline pipe = redisClient.pipelined()) {
pipe.zadd("likes:" + feedId, timestamp, userId);
pipe.hmset("feed:" + feedId, contentMap);
pipe.sync();
}
Asynchronous Processing
A lightweight wrapper around RocketMQ was developed to standardize message production and consumption. Features include:
- Batch and single-message consumpiton modes.
- Ordered message sending.
- Basic retry logic for broker throttling scenarios.