Build a High‑Performance Like System with Spring Boot, Redis, and Quartz

This tutorial explains how to implement a like/unlike feature using Spring Boot, Redis caching, and periodic persistence to a database with Quartz, covering data modeling, key design, service layers, controller APIs, and scheduled synchronization.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Build a High‑Performance Like System with Spring Boot, Redis, and Quartz

0. Preface

This article demonstrates how to use Spring Boot and Redis to implement a like/unlike cache with a timed persistence interface that writes data back to the database every two hours.

1. Project Directory Structure

2. Redis Cache Like Messages

2.1 Design Idea

When a user likes an item, the status is set to 0 and the likeCount is incremented; when the user unlikes, the status becomes 1 and the count is not incremented.

2.1.1 Redis Key‑Value Design

Use a Hash to store like information:

key: {infoId}::{likeUserId} value: a HashMap containing status (0 for like, 1 for unlike) and updateTime timestamp

Like count is stored separately:

key: {infoId} value: integer like count

2.2 Like Process

User sends a like request to the backend.

Backend checks the like status in Redis.

If the key does not exist or the status is already liked, Redis stores or updates the like information and increments the like count.

One like request completes.

2.3 Unlike Process

User sends an unlike request.

Backend checks the status in Redis.

Updates the like information and decrements the like count accordingly.

One unlike request completes.

3. Core Code Implementation

3.1 Redis Wrapper

Implementation details are omitted; refer to the linked blog.

https://www.cnblogs.com/caizhaokai/p/11037610.html

3.2 Utility Classes

3.2.1 Timestamp to LocalDateTime

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
 * Utility class to convert timestamps to LocalDateTime.
 */
public class LocalDateTimeConvertUtil {
    public static LocalDateTime getDateTimeOfTimestamp(long timestamp) {
        Instant instant = Instant.ofEpochMilli(timestamp);
        ZoneId zone = ZoneId.systemDefault();
        return LocalDateTime.ofInstant(instant, zone);
    }
}

3.2.2 RedisKey Utils

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";
    /**
     * Generate a key by concatenating infoId and likeUserId with "::".
     */
    public static String getLikedKey(String infoId, String likeUserId) {
        return infoId + "::" + likeUserId;
    }
}

3.2.3 DTOs

// UserLikesDTO.java
@Data @AllArgsConstructor @NoArgsConstructor
public class UserLikesDTO {
    private String infoId;
    private String likeUserId;
    private Integer status;
    private LocalDateTime updateTime;
}
// UserLikeCountDTO.java
@Data @AllArgsConstructor @NoArgsConstructor
public class UserLikeCountDTO {
    private String infoId;
    private Integer likeCount;
}

3.2.4 Service Interface

public interface RedisService {
    Integer getLikeStatus(String infoId, String likeUserId);
    void saveLiked2Redis(String infoId, String likeUserId);
    void unlikeFromRedis(String infoId, String likeUserId);
    void deleteLikedFromRedis(String infoId, String likeUserId);
    void in_decrementLikedCount(String infoId, Integer delta);
    List<UserLikesDTO> getLikedDataFromRedis();
    List<UserLikeCountDTO> getLikedCountFromRedis();
}

3.2.5 Service Implementation

@Service("redisService")
@Slf4j
public class RedisServiceImpl implements RedisService {
    @Autowired
    private HashOperations<String, String, Object> redisHash;
    @Override
    public Integer getLikeStatus(String infoId, String likeUserId) {
        if (redisHash.hasKey(RedisKeyUtils.MAP_KEY_USER_LIKED, RedisKeyUtils.getLikedKey(infoId, likeUserId))) {
            HashMap<String, Object> map = (HashMap<String, Object>) redisHash.get(RedisKeyUtils.MAP_KEY_USER_LIKED, RedisKeyUtils.getLikedKey(infoId, likeUserId));
            return (Integer) map.get("status");
        }
        return CONSTANT.LikedStatusEum.NOT_EXIST.getCode();
    }
    @Override
    public void saveLiked2Redis(String infoId, String likeUserId) {
        String key = RedisKeyUtils.getLikedKey(infoId, likeUserId);
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", CONSTANT.LikedStatusEum.LIKE.getCode());
        map.put("updateTime", System.currentTimeMillis());
        redisHash.put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, map);
    }
    @Override
    public void unlikeFromRedis(String infoId, String likeUserId) {
        String key = RedisKeyUtils.getLikedKey(infoId, likeUserId);
        HashMap<String, Object> map = new HashMap<>();
        map.put("status", CONSTANT.LikedStatusEum.UNLIKE.getCode());
        map.put("updateTime", System.currentTimeMillis());
        redisHash.put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, map);
    }
    @Override
    public void deleteLikedFromRedis(String infoId, String likeUserId) {
        String key = RedisKeyUtils.getLikedKey(infoId, likeUserId);
        redisHash.delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
    }
    @Override
    public void in_decrementLikedCount(String infoId, Integer delta) {
        redisHash.increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, infoId, delta);
    }
    @Override
    public List<UserLikesDTO> getLikedDataFromRedis() {
        Cursor<Map.Entry<String, Object>> cursor = redisHash.scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);
        List<UserLikesDTO> list = new ArrayList<>();
        while (cursor.hasNext()) {
            Map.Entry<String, Object> entry = cursor.next();
            String key = entry.getKey();
            String[] split = key.split("::");
            String infoId = split[0];
            String likeUserId = split[1];
            HashMap<String, Object> map = (HashMap<String, Object>) entry.getValue();
            Integer status = (Integer) map.get("status");
            long updateTimeStamp = (long) map.get("updateTime");
            LocalDateTime updateTime = LocalDateTimeConvertUtil.getDateTimeOfTimestamp(updateTimeStamp);
            UserLikesDTO dto = new UserLikesDTO(infoId, likeUserId, status, updateTime);
            list.add(dto);
            redisHash.delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
        }
        return list;
    }
    @Override
    public List<UserLikeCountDTO> getLikedCountFromRedis() {
        Cursor<Map.Entry<String, Object>> cursor = redisHash.scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);
        List<UserLikeCountDTO> list = new ArrayList<>();
        while (cursor.hasNext()) {
            Map.Entry<String, Object> entry = cursor.next();
            String key = entry.getKey();
            UserLikeCountDTO dto = new UserLikeCountDTO(key, (Integer) entry.getValue());
            list.add(dto);
            redisHash.delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);
        }
        return list;
    }
}

3.2.6 Controller & API

@RestController
@RequestMapping("/test/")
public class LikeController {
    @Autowired
    private LikeService likeService;
    @PostMapping("like")
    public CommonResponse<Object> likeInfo(String infoId, String userId) {
        return likeService.likeInfo(infoId, userId);
    }
    @PostMapping("dislike")
    public CommonResponse<Object> dislikeInfo(String infoId, String userId) {
        return likeService.dislikeInfo(infoId, userId);
    }
}

4. Redis Timed Persistence

4.1 Design Idea

4.1.1 Database Design

# view_item table
DROP TABLE IF EXISTS `view_item`;
CREATE TABLE `view_item` (
  `id` varchar(32) NOT NULL COMMENT 'content id',
  `create_user` varchar(50) DEFAULT NULL COMMENT 'creator',
  `like_count` int(11) DEFAULT NULL COMMENT 'like count',
  `cmt_count` int(11) DEFAULT NULL COMMENT 'comment count',
  `share_count` int(11) DEFAULT NULL COMMENT 'share count',
  `create_time` datetime DEFAULT NULL COMMENT 'creation time',
  `update_time` datetime DEFAULT NULL COMMENT 'update time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

# user_likes table
DROP TABLE IF EXISTS `user_likes`;
CREATE TABLE `user_likes` (
  `id` varchar(32) NOT NULL COMMENT 'like record id',
  `info_id` varchar(32) DEFAULT NULL COMMENT 'liked content id',
  `like_user_id` varchar(32) DEFAULT NULL COMMENT 'liker id',
  `status` tinyint(4) DEFAULT '0' COMMENT '0 like, 1 unlike',
  `create_time` datetime DEFAULT NULL COMMENT 'creation time',
  `update_time` datetime DEFAULT NULL COMMENT 'update time',
  PRIMARY KEY (`id`),
  UNIQUE KEY `agdkey` (`like_user_id`,`info_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.1.2 Process Flow

Iterate Redis like entries and update the database status.

Check if the record exists in the DB; if not, insert it and update the content's like count.

If it exists, compare DB and Redis statuses and adjust counts accordingly.

Persist the like count from Redis to the DB.

4.2 Core Code Implementation

4.2.1 Bean Definitions

@Data @TableName("user_likes")
public class UserLikes {
    @TableId
    private String id;
    @TableField("info_id")
    private String infoId;
    @TableField("like_user_id")
    private String likeUserId;
    private Integer status;
    @TableField("create_time")
    private LocalDateTime createTime;
    @TableField("update_time")
    private LocalDateTime updateTime;
}

@Data @TableName("view_item")
public class ViewItem {
    @TableId
    private String id;
    @TableField("create_user")
    private String createUser;
    @TableField("like_count")
    private Integer likeCount;
    @TableField("cmt_count")
    private Integer cmtCount;
    @TableField("share_count")
    private Integer shareCount;
    @TableField("create_time")
    private LocalDateTime createTime;
    @TableField("update_time")
    private LocalDateTime updateTime;
}

4.2.2 DB Service Interface

public interface DBService {
    Boolean save(UserLikes userLike);
    Boolean update(UserLikes userLike);
    Page<UserLikes> getLikedListByInfoId(String infoId, int pageNum, int pageSize);
    Page<UserLikes> getLikedListByLikeUserId(String likeUserId, int pageNum, int pageSize);
    UserLikes getByInfoIdAndLikeUserId(String infoId, String likeUserId);
    void transLikedFromRedis2DB();
    void transLikedCountFromRedis2DB();
}

4.2.3 DB Service Implementation

@Service("dbService")
@Slf4j
public class DBServiceImpl implements DBService {
    @Autowired
    private RedisService redisService;
    @Autowired
    private UserLikesMapper userLikesMapper;
    @Autowired
    private ViewItemMapper viewItemMapper;
    @Autowired
    private Sid sid;

    @Override
    public Boolean save(UserLikes userLike) {
        return userLikesMapper.insert(userLike) > 0;
    }

    @Override
    public Boolean update(UserLikes userLike) {
        UpdateWrapper<UserLikes> wrapper = new UpdateWrapper<>();
        wrapper.set("status", userLike.getStatus())
               .set("update_time", userLike.getUpdateTime())
               .eq("id", userLike.getId());
        return userLikesMapper.update(userLike, wrapper) > 0;
    }

    @Override
    public Page<UserLikes> getLikedListByInfoId(String infoId, int pageNum, int pageSize) {
        Page<UserLikes> page = new Page<>();
        page.setCurrent(pageNum);
        page.setSize(pageSize);
        QueryWrapper<UserLikes> qw = new QueryWrapper<>();
        qw.eq("info_id", infoId);
        return userLikesMapper.selectPage(page, qw);
    }

    @Override
    public Page<UserLikes> getLikedListByLikeUserId(String likeUserId, int pageNum, int pageSize) {
        Page<UserLikes> page = new Page<>();
        page.setCurrent(pageNum);
        page.setSize(pageSize);
        QueryWrapper<UserLikes> qw = new QueryWrapper<>();
        qw.eq("like_user_id", likeUserId);
        return userLikesMapper.selectPage(page, qw);
    }

    @Override
    public UserLikes getByInfoIdAndLikeUserId(String infoId, String likeUserId) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("info_id", infoId);
        map.put("like_user_id", likeUserId);
        try {
            return userLikesMapper.selectByMap(map).get(0);
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public void transLikedFromRedis2DB() {
        List<UserLikesDTO> list = redisService.getLikedDataFromRedis();
        if (CollectionUtils.isEmpty(list)) return;
        for (UserLikesDTO dto : list) {
            UserLikes dbRecord = getByInfoIdAndLikeUserId(dto.getInfoId(), dto.getLikeUserId());
            if (dbRecord == null) {
                if (!save(userLikesDTOtoUserLikes(dto))) {
                    log.info("Failed to insert like record from cache");
                    return;
                }
            } else {
                if (dbRecord.getStatus().equals(dto.getStatus())) {
                    redisService.in_decrementLikedCount(dto.getInfoId(), -1);
                } else {
                    if (dbRecord.getStatus().equals(CONSTANT.LikedStatusEum.LIKE.getCode())) {
                        dbRecord.setStatus(CONSTANT.LikedStatusEum.UNLIKE.getCode());
                        redisService.in_decrementLikedCount(dto.getInfoId(), -1);
                    } else if (dbRecord.getStatus().equals(CONSTANT.LikedStatusEum.UNLIKE.getCode())) {
                        dbRecord.setStatus(CONSTANT.LikedStatusEum.LIKE.getCode());
                        redisService.in_decrementLikedCount(dto.getInfoId(), 1);
                    }
                    dbRecord.setUpdateTime(LocalDateTime.now());
                    if (!update(dbRecord)) {
                        log.info("Failed to update like record");
                        return;
                    }
                }
            }
        }
    }

    @Override
    public void transLikedCountFromRedis2DB() {
        List<UserLikeCountDTO> list = redisService.getLikedCountFromRedis();
        if (CollectionUtils.isEmpty(list)) return;
        for (UserLikeCountDTO dto : list) {
            ViewItem item = viewItemMapper.selectById(dto.getInfoId());
            if (item != null) {
                Integer newCount = (item.getLikeCount() == null ? 0 : item.getLikeCount()) + dto.getLikeCount();
                item.setLikeCount(newCount);
                UpdateWrapper<ViewItem> wrapper = new UpdateWrapper<>();
                wrapper.set("like_count", newCount).eq("id", item.getId());
                viewItemMapper.update(item, wrapper);
            }
        }
    }

    private UserLikes userLikesDTOtoUserLikes(UserLikesDTO dto) {
        UserLikes entity = new UserLikes();
        entity.setId(sid.nextShort());
        BeanUtils.copyProperties(dto, entity);
        entity.setCreateTime(LocalDateTime.now());
        entity.setUpdateTime(LocalDateTime.now());
        return entity;
    }
}

5. Scheduled Database Update

5.1 Quartz Job

@Slf4j
public class CronUtil extends QuartzJobBean {
    @Autowired
    private DBService dbService;
    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()));
        dbService.transLikedFromRedis2DB();
        dbService.transLikedCountFromRedis2DB();
    }
}

5.2 Quartz Configuration

@Configuration
public class QuartzConfig {
    private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz";
    @Bean
    public JobDetail quartzDetail() {
        return JobBuilder.newJob(CronUtil.class)
                .withIdentity(LIKE_TASK_IDENTITY)
                .storeDurably()
                .build();
    }
    @Bean
    public Trigger quartzTrigger() {
        SimpleScheduleBuilder schedule = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInHours(2)
                .repeatForever();
        return TriggerBuilder.newTrigger()
                .forJob(quartzDetail())
                .withIdentity(LIKE_TASK_IDENTITY)
                .withSchedule(schedule)
                .build();
    }
}

6. Source Code Repository & References

Project source code: https://github.com/WuYiheng-Og/redislike

References:

https://cloud.tencent.com/developer/article/1445905

https://www.cnblogs.com/caizhaokai/p/11037610.html

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.

javabackend-developmentredisSpring BootLike SystemQuartz Scheduler
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

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.