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.

samdeepthink
samdeepthink
samdeepthink
Designing a Hundred‑Million‑Scale Like System: Graph Store + KV Approach

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.

Architecture diagram
Architecture diagram

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 1

Batch 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Redisgraph databasemysqlscalable architectureidempotenceKV storelike system
samdeepthink
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.