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.

21CTO
21CTO
21CTO
Building a Scalable Like System with Spring Cloud, Redis, and Quartz

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-server

1.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:

Redis data structures
Redis data structures

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.

Redis hash example
Redis hash example
Redis hash visualized
Redis hash visualized

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.

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.

databaseredisSpringBootQuartzLike System
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

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.