Building a Scalable Like System with Spring Cloud, Redis, and Quartz
This article explains how to design and implement a high‑frequency like/unlike feature for large social platforms using Spring Cloud, Redis caching, MySQL persistence, and Quartz scheduled jobs, covering Redis setup, data modeling, service interfaces, database schema, and periodic data synchronization.
Likes are a familiar feature in social products like WeChat, but implementing a robust like system for large‑scale platforms requires handling distributed storage, caching, multi‑IDC data consistency, and routing algorithms.
The article presents a design based on Spring Cloud where user actions (like or unlike) are first stored in Redis and then persisted to a relational database every two hours.
High‑frequency like/unlike operations should not hit the database directly; caching with Redis is essential.
The tutorial is divided into four parts: Redis cache design and implementation, database design, database operations, and a scheduled task for persistence.
1. Redis Cache Design and Implementation
1.1 Redis Installation and Running
Refer to Redis installation tutorials for details.
Run Redis with Docker: docker run -d -p 6379:6379 redis:4.0.8 If Redis is already installed, start it with the appropriate command.
redis-server1.2 Integrating Redis with a Spring Boot Project
1. Add the dependency in pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>2. Annotate the main class with @EnableCaching:
@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
@EnableFeignClients(basePackages = "com.solo.coderiver.project.client")
@EnableCaching
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}3. Create a Redis configuration class RedisConfig:
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import java.net.UnknownHostException;
@Configuration
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}1.3 Redis Data Structure Types
Redis can store a key with five different data structures: String, List, Set, Hash, and Zset.
Below is a brief overview of each type:
1.4 Like Data Storage Format in Redis
Two kinds of data are stored in Redis: a record of likedUserId, likedPostId, and status; and a simple counter for how many times each user has been liked.
Hash is the most suitable structure because it allows all like records to be stored under a single key and retrieved easily.
The key format is likedUserId::likedPostId. When a user likes, the value is 1; when the user unlikes, the value is 0. Splitting the key by :: yields the two IDs.
1.5 Redis Operations
Operations are encapsulated in the RedisService interface:
public interface RedisService {
/** Like. Status = 1 */
void saveLiked2Redis(String likedUserId, String likedPostId);
/** Unlike. Status = 0 */
void unlikeFromRedis(String likedUserId, String likedPostId);
/** Delete a like record */
void deleteLikedFromRedis(String likedUserId, String likedPostId);
/** Increment like count for a user */
void incrementLikedCount(String likedUserId);
/** Decrement like count for a user */
void decrementLikedCount(String likedUserId);
/** Get all like records from Redis */
List<UserLike> getLikedDataFromRedis();
/** Get all like‑count records from Redis */
List<LikedCountDTO> getLikedCountFromRedis();
}The implementation RedisServiceImpl uses RedisTemplate to manipulate hashes and counters.
public class RedisServiceImpl implements RedisService {
@Autowired
RedisTemplate redisTemplate;
@Autowired
LikedService likedService;
@Override
public void saveLiked2Redis(String likedUserId, String likedPostId) {
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());
}
// ... other methods omitted for brevity ...
}Utility and Enum Classes RedisKeyUtils builds keys, and LikedStatusEnum defines LIKE (1) and UNLIKE (0) statuses.
public class RedisKeyUtils {
public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED";
public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT";
public static String getLikedKey(String likedUserId, String likedPostId) {
return likedUserId + "::" + likedPostId;
}
} public enum LikedStatusEnum {
LIKE(1, "点赞"),
UNLIKE(0, "取消点赞/未点赞");
private Integer code;
private String msg;
LikedStatusEnum(Integer code, String msg) { this.code = code; this.msg = msg; }
public Integer getCode() { return code; }
}2. Database Design
The table must contain at least three fields: the ID of the liked user, the ID of the user who liked, and the status, plus primary key, creation time, and update time.
create table `user_like` (
`id` int not null auto_increment,
`liked_user_id` varchar(32) not null comment '被点赞的用户id',
`liked_post_id` varchar(32) not null comment '点赞的用户id',
`status` tinyint(1) default '1' comment '点赞状态,0取消,1点赞',
`create_time` timestamp not null default current_timestamp comment '创建时间',
`update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
primary key (`id`),
index `liked_user_id`(`liked_user_id`),
index `liked_post_id`(`liked_post_id`)
) comment '用户点赞表';The corresponding JPA entity:
@Entity
@Data
public class UserLike {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String likedUserId; // 被点赞的用户id
private String likedPostId; // 点赞的用户id
private Integer status = LikedStatusEnum.UNLIKE.getCode();
public UserLike() {}
public UserLike(String likedUserId, String likedPostId, Integer status) {
this.likedUserId = likedUserId;
this.likedPostId = likedPostId;
this.status = status;
}
}3. Database Operations
Operations are defined in LikedService and implemented in LikedServiceImpl.
public interface LikedService {
UserLike save(UserLike userLike);
List<UserLike> saveAll(List<UserLike> list);
Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable);
Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable);
UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId);
void transLikedFromRedis2DB();
void transLikedCountFromRedis2DB();
} @Service
public class LikedServiceImpl implements LikedService {
@Autowired
UserLikeRepository likeRepository;
@Autowired
RedisService redisService;
@Autowired
UserService userService;
@Transactional
public UserLike save(UserLike userLike) { return likeRepository.save(userLike); }
@Transactional
public List<UserLike> saveAll(List<UserLike> list) { return likeRepository.saveAll(list); }
public Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable) {
return likeRepository.findByLikedUserIdAndStatus(likedUserId, LikedStatusEnum.LIKE.getCode(), pageable);
}
public Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable) {
return likeRepository.findByLikedPostIdAndStatus(likedPostId, LikedStatusEnum.LIKE.getCode(), pageable);
}
public UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId) {
return likeRepository.findByLikedUserIdAndLikedPostId(likedUserId, likedPostId);
}
@Transactional
public void transLikedFromRedis2DB() {
List<UserLike> list = redisService.getLikedDataFromRedis();
for (UserLike like : list) {
UserLike existing = getByLikedUserIdAndLikedPostId(like.getLikedUserId(), like.getLikedPostId());
if (existing == null) {
save(like);
} else {
existing.setStatus(like.getStatus());
save(existing);
}
}
}
@Transactional
public void transLikedCountFromRedis2DB() {
List<LikedCountDTO> list = redisService.getLikedCountFromRedis();
for (LikedCountDTO dto : list) {
UserInfo user = userService.findById(dto.getId());
if (user != null) {
Integer likeNum = user.getLikeNum() + dto.getCount();
user.setLikeNum(likeNum);
userService.updateInfo(user);
}
}
}
}4. Scheduled Task for Persistence
Quartz is used to run the synchronization every two hours.
4.1 Add Maven Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>4.2 Quartz Configuration
@Configuration
public class QuartzConfig {
private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz";
@Bean
public JobDetail quartzDetail() {
return JobBuilder.newJob(LikeTask.class)
.withIdentity(LIKE_TASK_IDENTITY)
.storeDurably()
.build();
}
@Bean
public Trigger quartzTrigger() {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInHours(2)
.repeatForever();
return TriggerBuilder.newTrigger()
.forJob(quartzDetail())
.withIdentity(LIKE_TASK_IDENTITY)
.withSchedule(scheduleBuilder)
.build();
}
}4.3 Quartz Job Implementation
@Slf4j
public class LikeTask extends QuartzJobBean {
@Autowired
LikedService likedService;
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
log.info("LikeTask-------- {}", sdf.format(new Date()));
likedService.transLikedFromRedis2DB();
likedService.transLikedCountFromRedis2DB();
}
}With this scheduled job, Redis‑cached like data is automatically synchronized to the relational database every two hours, ensuring consistency while keeping the hot path fast.
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.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
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.
