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