Designing a Hundred‑Million‑Scale Like System: Graph Store + KV Approach
The article analyzes the requirements of a massive‑scale like feature, compares small‑scale MySQL implementations with the graph‑plus‑KV architectures used by ByteDance, Kuaishou and Xiaohongshu, and presents a practical hybrid solution based on Redis, MySQL and batch processing to handle hot content and super‑node challenges.
Understanding the Requirements
A like operation involves six core tasks: like/unlike, total count display, user‑like status, user’s liked list, author’s total likes, and idempotent handling of repeated clicks. These are essentially different views of a single user‑content relationship.
Check if the edge exists to determine if the user has liked the content.
Count edges pointing to a content node to obtain the total likes.
Traverse outgoing edges from a user node to list liked items.
At a scale of hundreds of millions of daily active users, read traffic can reach tens of millions QPS, while write traffic is comparatively low but concentrated on hot items.
Small‑Scale Solution: MySQL Table
For modest traffic a single MySQL table with columns id, user_id, content_id, content_type, status, create_time and a unique index on (user_id, content_id, content_type) suffices. Likes are inserted, the like_count column on the content table is incremented within a transaction, and the row lock on hot content becomes a bottleneck once millions of users target the same item.
Why Traditional Approaches Fail at Massive Scale
Two failure points emerge:
Hot content causes row‑level lock contention on the like_count column.
Checking "has the user liked?" requires an index scan on a table that may contain tens of millions of rows, which is too heavy for per‑content reads.
Typical mitigation adds a Redis cache for counts and a Redis Set for like status, with asynchronous MySQL updates, but this still does not scale to the billions‑level workloads of TikTok, Kuaishou and Xiaohongshu.
Large‑Scale Designs from Leading Companies
ByteDance (TikTok) – ByteGraph + Abase
ByteDance models likes as edges in a graph database (ByteGraph). The graph stores billions of vertices and trillions of edges, handling read‑to‑write ratios of nearly 100:1. Counting uses a separate KV store (Abase) where the INCRBY command implements the incremental counter.
"String type supports IncrBy, which is the typical model for like count scenarios." – ByteDance documentation
Kuaishou – KGraph
KGraph stores edges exceeding 10 trillion, delivering up to 20 million QPS on a 12‑node cluster with sub‑millisecond latency. Super‑node edges that exceed a threshold are split into multiple KV shards organized like a B‑tree, balancing read/write amplification.
Xiaohongshu – REDtao
REDtao combines a distributed graph cache with a sharded MySQL persistence layer. After migration, MySQL QPS dropped by 70 % while cache hit rates stayed above 90 %. Super‑node handling keeps only the latest 1 000 edges per relationship in cache, relying on temporal locality.
Key Takeaways from the Three Companies
Likes are naturally a graph relationship.
Read‑heavy workloads benefit from one‑hop graph queries.
Super‑node problems have mature graph‑storage solutions.
Thus, the complexity of a like system lies in managing both counting and relationship data.
Practical Hybrid Solution for Most Projects
For systems that are not yet at the hundred‑million‑DAU level, a combination of Redis and MySQL provides sufficient performance while keeping architecture simple.
Overall Architecture
Requests flow through a gateway to the Like service, which first checks a local in‑process cache, then Redis, and finally MySQL. Write operations are performed atomically in Redis via a Lua script and returned immediately; persistence to MySQL is handled asynchronously via a message queue.
Write Path: Lua Script + Async Persistence
The Lua script ensures three steps—check, write, increment—are executed atomically, preventing duplicate likes.
public void like(Long userId, Long contentId, Integer contentType) {
// Lua script atomically checks, writes like record, and increments count
Long result = redisTemplate.execute(LIKE_SCRIPT,
Lists.newArrayList(recordKey(contentId), countKey(contentId)),
String.valueOf(userId));
// 0 means already liked, idempotent return
if (result == 0L) {
return;
}
// Asynchronously persist to MySQL
likeMqProducer.send(new LikeEvent(userId, contentId, contentType, LIKE));
}The script itself:
-- Return 0 if already liked (idempotent guard)
if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then
return 0
end
redis.call('SADD', KEYS[1], ARGV[1])
redis.call('INCR', KEYS[2])
return 1Batch Persistence
Consumers aggregate events per content for a few seconds, merging them into a single INCR update and a batch insert of like records. Unique indexes on (content_id, content_type, user_id) guarantee idempotence.
public void onMessage(List<LikeEvent> events) {
// Aggregate counts per content
Map<Long, LongAdder> counter = new HashMap<>();
events.forEach(e -> counter
.computeIfAbsent(e.getContentId(), k -> new LongAdder())
.increment());
// Batch insert like records (duplicates fail on unique index)
likeRecordMapper.insertBatch(events);
// Apply aggregated count updates
counter.forEach((contentId, adder) ->
contentMapper.incrLikeCount(contentId, adder.intValue()));
}Read Path: Three‑Layer Cache
Reads first consult a local hot‑key cache (seconds TTL), then Redis, and finally MySQL if needed.
public long getLikeCount(Long contentId) {
Long count = hotLocalCache.getIfPresent(contentId);
if (count != null) {
return count;
}
String value = redisTemplate.opsForValue().get(countKey(contentId));
return value != null ? Long.parseLong(value) : loadFromDb(contentId);
}For "has liked" checks, the Redis Set may become a large key for ultra‑hot content; the solution is to cache only the most recent N edges (e.g., 10 000) and fall back to MySQL for older entries, mirroring Xiaohongshu’s approach.
Conclusion
Graph‑plus‑KV architectures are driven by the data shape (relationship), extreme read‑write skew, and power‑law distribution of hot items. While they are the ultimate answer for hundred‑million‑scale systems, most projects can start with a Redis‑MySQL hybrid, adding caching layers, Lua‑based idempotence, and batch persistence until the workload forces a migration to a dedicated graph store.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
samdeepthink
Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
