[AI Readability Summary] This article shows how to build a high-concurrency like system with Spring Boot and Redis ZSet. It solves three core problems: enforcing one like per user, highlighting the current user’s like state, and generating a real-time leaderboard. By using ZSet member uniqueness and timestamp-based ordering, the design reduces database pressure while preserving interaction order.
Keywords: Redis ZSet, high-concurrency likes, leaderboard.
The technical specification snapshot clarifies the system boundaries
| Parameter | Description |
|---|---|
| Primary Language | Java |
| Core Framework | Spring Boot |
| Data Storage | MySQL + Redis |
| Key Data Structure | Redis ZSet |
| Access Protocol | HTTP/REST |
| Sorting Strategy | score stores a millisecond timestamp |
| Leaderboard Size | Top 5 earliest users who liked |
| Article Popularity | 687 views / 27 likes |
| Core Dependencies | StringRedisTemplate, MyBatis-Plus, Hutool |
This like system essentially uses Redis to absorb high-frequency write pressure
In a social content discovery scenario, a like is not just a counter. It is a stateful interaction. The system must record the total like count, determine whether the current user has liked the post, and generate a time-ordered leaderboard of users.
If everything goes through MySQL, the database must handle high-frequency writes, deduplication checks, and sorted queries at the same time. That approach is expensive and introduces unpredictable latency. A more practical design places like-state handling in Redis and keeps the final count persisted in the database.
The business flow can be decomposed into three independent actions
- Publish a review or discovery post, and persist the content into
tb_blog. - Let users like or unlike the post, and update both Redis and the database counter.
- Read the like state on the detail page and query the Top 5 users who liked the post.
// The three core goals of the like system
boolean uniqueLike = true; // One like per user
boolean highPerformance = true; // Fast response under high concurrency
boolean sortableRanking = true; // Support leaderboard ranking
This code summarizes the three design constraints of the system: idempotency, performance, and ordering.
Redis ZSet is the most suitable data structure for this scenario
A Set can only guarantee unique members. It cannot express who liked first. A ZSet preserves uniqueness while adding a score for each member, which makes it a natural fit for storing time order.
The design here is straightforward: use member = userId and score = current millisecond timestamp. This lets the system determine whether a user has already liked the post and fetch the earliest users in chronological order.
The ZSet key model should remain stable and predictable
Use blog:liked:{blogId} as the key pattern. With this structure, each post maps to its own sorted set, which keeps queries and maintenance clear.
String key = "blog:liked:" + blogId; // Build an isolated like set for each blog post
Double score = stringRedisTemplate.opsForZSet()
.score(key, userId.toString()); // Check whether the user has already liked the post
This code uses score() as an existence check, which is the critical entry point for implementing idempotent toggling.
Like and unlike operations must be designed around idempotent toggling
The core of the like API is not simply incrementing a counter. It must first check the current state, then switch that state. If the user has not liked the post, the system updates the database with liked + 1 and writes the userId into the ZSet. Otherwise, it performs the unlike operation.
For update order, a common practice is to update the database first and then update Redis. This gives the counter change a clearer persistence result.
The critical implementation of the like logic should follow the shortest execution path
@Override
public Result likeBlog(Long id) {
Long userId = UserHolder.getUser().getId(); // Get the currently logged-in user
String key = BLOG_LIKED_KEY + id;
Double score = stringRedisTemplate.opsForZSet()
.score(key, userId.toString()); // Check whether the post is already liked
if (score == null) {
boolean ok = update().setSql("liked = liked + 1")
.eq("id", id).update(); // Increment the like count in the database
if (ok) {
stringRedisTemplate.opsForZSet()
.add(key, userId.toString(), System.currentTimeMillis()); // Write the like record and timestamp
}
} else {
boolean ok = update().setSql("liked = liked - 1")
.eq("id", id).update(); // Decrement the like count in the database
if (ok) {
stringRedisTemplate.opsForZSet()
.remove(key, userId.toString()); // Remove the like record
}
}
return Result.ok();
}
This code completes the two-way toggle between like and unlike while using ZSet to guarantee a unique state for each user.
The highlighted like state must be filled through the isLike field during reads
Many implementations focus only on the like action itself and ignore the detail-page rendering path. In practice, the frontend needs an isLike field to decide whether the heart icon should be highlighted. That is read-path logic and should not be mixed with write-path logic.
When a user opens the detail page, the backend should query Redis again to determine whether the current userId exists in the corresponding ZSet. If the user is not logged in, return false directly to avoid a null pointer exception.
public void fillIsLike(Blog blog) {
UserDTO user = UserHolder.getUser();
if (user == null) {
blog.setIsLike(false); // Return not liked directly for unauthenticated users
return;
}
String key = BLOG_LIKED_KEY + blog.getId();
Double score = stringRedisTemplate.opsForZSet()
.score(key, user.getId().toString()); // Read the like state
blog.setIsLike(score != null);
}
This code fills the isLike field during reads and directly affects frontend interaction correctness.
Leaderboard queries must explicitly fix MySQL order loss
The order of the Top 5 user IDs returned by Redis is meaningful because it represents the sequence of likes. However, a MySQL IN query does not preserve input order. By default, it may return rows sorted by primary key.
For that reason, the leaderboard query cannot rely on where id in (...) alone. It must append ORDER BY FIELD(id, ...) to force the database to return user information in the same order Redis produced.
The correct Top 5 leaderboard query depends on two-stage ordering
@Override
public Result queryBlogLikes(Long id) {
String key = BLOG_LIKED_KEY + id;
Set
<String> top5 = stringRedisTemplate.opsForZSet()
.range(key, 0, 4); // Get the first five earliest likes
if (top5 == null || top5.isEmpty()) {
return Result.ok(Collections.emptyList());
}
List
<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
String idStr = StrUtil.join(",", ids);
List
<UserDTO> users = userService.query()
.in("id", ids)
.last("ORDER BY FIELD(id, " + idStr + ")") // Force Redis order to be preserved
.list()
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(users);
}
This code first uses Redis for ordering, then uses MySQL to enrich user details, while preserving the original order through FIELD.
Consistency under high concurrency should be understood through eventual consistency
Under extreme concurrency, multiple requests may read score == null at the same time. However, the database update liked = liked + 1 is an atomic operation, which prevents incorrect counts caused by simple overwrite writes.
What the system must accept is a very short inconsistency window between Redis and MySQL. For a like feature, this delay is usually acceptable. If the business requires stronger consistency, then consider Lua scripts, message queues, or transactional compensation.
The two most common pitfalls should be fixed first
- Calling
UserHolder.getUser().getId()directly for an unauthenticated request can trigger an NPE. INqueries do not preserve order, which can distort leaderboard results.- Inconsistent Redis member types, such as storing strings and querying with
Long, can cause false negatives for like-state checks.
String redisMember = userId.toString(); // Keep the Redis member type consistent
boolean safe = redisMember != null && !redisMember.isEmpty();
This code highlights a high-frequency implementation detail: Redis member types must be consistently stored as strings.
The images mainly serve as visual decoration rather than architecture diagrams
AI Visual Insight: This image appears to function more as a cover or decorative illustration. It does not show system topology, call chains, or data-structure relationships, so it adds limited value to the technical implementation itself.
AI Visual Insight: This GIF looks more like an animated mood image. It does not demonstrate API calls, Redis command execution, or leaderboard interaction flow, so it works better as visual guidance than as technical evidence.
This approach fits medium- to high-frequency social interaction scenarios
When the business simultaneously requires idempotent likes, highlighted state, and a real-time leaderboard, Redis ZSet provides clear advantages. It is not acting as a simple cache. It participates directly in business-state checks and ordering logic.
If the system later expands into feed streams, follow notifications, or infinite scrolling pagination, it can continue to reuse the time-ordering characteristics of ZSet and upgrade from offset pagination to score-based cursor pagination.
FAQ structured Q&A
1. Why use Redis ZSet instead of Redis Set?
A Set can only deduplicate. It cannot sort. A like leaderboard must return Top N users by time, so it needs ZSet score values to store timestamps.
2. Can I keep only the like state in Redis and skip database updates?
That is not recommended. Redis is well suited for absorbing high-frequency state changes, but the database should still persist stable counters and final business results. Otherwise, persistence and analytics will both suffer.
3. Why does the leaderboard return users in the wrong order?
Because a MySQL IN query does not return rows in the same order as the input IDs by default. The fix is to append ORDER BY FIELD(id, ...) and explicitly preserve Redis order.
Core Summary: This article reconstructs a high-concurrency like system based on Spring Boot, Redis ZSet, and MySQL. It focuses on one-like-per-user idempotency, highlighted like state, preserving Top 5 leaderboard order, consistency under high concurrency, and defensive handling for unauthenticated requests.