How to Build a Scalable Like System with Spring Cloud, Redis, and Quartz
This tutorial walks through building a scalable like feature using Spring Cloud, Redis caching, MySQL persistence, and Quartz scheduling, covering Redis installation, Spring Boot integration, data modeling, service implementation, and periodic synchronization of high‑frequency like operations to the database.
Based on Spring Cloud, a like or unlike action is first stored in Redis and then persisted to the database every two hours.
Likes are high‑frequency operations; writing directly to the database on each request would degrade performance, so caching is required.
The article is divided into four parts:
Redis cache design and implementation
Database design
Database operations
Scheduled task for persistence
1. Redis Cache Design and Implementation
1.1 Redis Installation and Running
Install Redis according to relevant tutorials.
Docker command to run Redis: docker run -d -p 6379:6379 redis:4.0.8 If Redis is already installed, start it with:
redis-server1.2 Integrating Redis with Spring Boot
1. Add the dependency in pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>2. Add @EnableCaching on the main application class:
@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 the 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;
}
}Now Redis is ready for use in the Spring Boot project.
1.3 Redis Data Structure Types
Redis supports five data structures: String, List, Set, Hash, and Zset. The article uses Hash to store like data because it allows storing multiple fields under a single key.
1.4 Like Data Storage Format in Redis
Two kinds of data are stored in Redis:
A hash mapping likedUserId::likedPostId to a status (1 for like, 0 for unlike).
A hash counting how many times each user has been liked.
Example: when a user likes, the key likedUserId::likedPostId is stored with value 1; when unliked, the same key is stored with value 0. Splitting the key by :: retrieves the two IDs.
Visualization in RDM:
1.5 Redis Operations Service
The RedisService interface defines methods for saving, unliking, deleting, incrementing, decrementing, and retrieving like data.
public interface RedisService {
void saveLiked2Redis(String likedUserId, String likedPostId);
void unlikeFromRedis(String likedUserId, String likedPostId);
void deleteLikedFromRedis(String likedUserId, String likedPostId);
void incrementLikedCount(String likedUserId);
void decrementLikedCount(String likedUserId);
List<UserLike> getLikedDataFromRedis();
List<LikedCountDTO> getLikedCountFromRedis();
}The implementation RedisServiceImpl uses RedisTemplate to operate on the hash maps defined in RedisKeyUtils.
... (implementation code omitted for brevity) ...Utility class RedisKeyUtils defines constant map names and builds keys:
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;
}
}Enum LikedStatusEnum defines the like status values:
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; }
public String getMsg() { return msg; }
}2. Database Design
The MySQL table user_like stores the like records with fields for the liked user ID, the user who liked, the status, and timestamps.
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 UserLike maps these columns:
@Entity
@Data
public class UserLike {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String likedUserId;
private String likedPostId;
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
Repository interfaces and the LikedService provide CRUD methods, pagination, and methods to transfer data from Redis to the database.
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();
}The implementation LikedServiceImpl uses RedisService to fetch pending data and persists or updates records accordingly, handling both individual like records and like count updates.
... (implementation code omitted for brevity) ...4. Scheduled Task for Persistence
Quartz is used to run a job every two hours that calls LikedService to sync Redis data to the database.
@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();
}
} @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();
}
}The article ends by inviting feedback on the design and implementation.
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.
ITFLY8 Architecture Home
ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.
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.
