This article is based on spring cloud. After users send and cancel their likes, they first store them in Redis, and then read the likes from Redis every two hours and write them into the database for persistent storage.
The like function is available in many systems, but despite the small function, there are many things to consider if you want to do a good job.
Liking and canceling liking are high-frequency operations. If the database is read and written every time, a large number of operations will affect the database performance, so caching is required.
As for how often to get data from Redis and save it in the database, it depends on the actual situation of the project. I set it for two hours temporarily.
Project requirements need to check who likes them. Therefore, it is necessary to store the likes and the likes of each person. You can't simply count them.
The article is divided into four parts:
-
Redis cache design and Implementation
-
Database design
-
Database operation
-
Enable scheduled task persistence storage to the database
1, Redis cache design and Implementation
1.1 Redis installation and operation
Please refer to the relevant tutorials for Redis installation.
Talk about Docker installation and running Redis
docker run -d -p 6379:6379 redis:4.0.8
If Redis is already installed, open the command line and enter the command to start Redis
redis-server
1.2 integration of redis and SpringBoot projects
1. Introduce dependency in pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2. Add the comment @ EnableCaching on the startup 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. Write RedisConfig configuration class
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>(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<String, Object>(); 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; } }
So far, Redis has been configured in the SpringBoot project and can be used happily.
1.3 Redis data structure type
Redis can store mappings between keys and five different data structure types: String, List, Set, Hash and Zset.
The following is a brief introduction to the five data structure types:
1.4 storage format of likes data in Redis
Redis is used to store two kinds of data, one is to record the likes, likes and likes, and the other is to simply count how many times each user has been liked.
Because it is necessary to record the likes and the likes, as well as the like status (like, cancel like), it is also necessary to take out all the like data in Redis at fixed intervals, and analyze the most suitable Hash in the next Redis data format.
Because the data in Hash is stored in a key, you can easily take out all the likes data through this key. The data in this key can also be stored in the form of key value pairs to facilitate the storage of likes, likes and likes.
The id of the person who likes is likedPostId, the id of the person who is liked is likedUserId, the status is 1 when you like, and the status of canceling likes is 0. The id of the person who likes and the id of the person who is liked are used as keys, the two IDs are separated by::, and the like status is used as the value.
Therefore, if the user likes, the stored key is: likedUserId::likedPostId, and the corresponding value is 1. Cancel liking, the stored key is: likedUserId::likedPostId, and the corresponding value is 0. When getting data, cut the key with:: to get two IDS, which is also very convenient.
This is what you see in the visualizer RDM
1.5 Redis operation
The specific operation methods are encapsulated in the RedisService interface
RedisService.java
import com.solo.coderiver.user.dataobject.UserLike; import com.solo.coderiver.user.dto.LikedCountDTO; import java.util.List; public interface RedisService { /** * give the thumbs-up. Status is 1 * @param likedUserId * @param likedPostId */ void saveLiked2Redis(String likedUserId, String likedPostId); /** * Cancel like. Change status to 0 * @param likedUserId * @param likedPostId */ void unlikeFromRedis(String likedUserId, String likedPostId); /** * Delete a likes data from Redis * @param likedUserId * @param likedPostId */ void deleteLikedFromRedis(String likedUserId, String likedPostId); /** * The number of likes of the user is increased by 1 * @param likedUserId */ void incrementLikedCount(String likedUserId); /** * The number of likes of the user is reduced by 1 * @param likedUserId */ void decrementLikedCount(String likedUserId); /** * Get all the likes data stored in Redis * @return */ List<UserLike> getLikedDataFromRedis(); /** * Get the number of all likes stored in Redis * @return */ List<LikedCountDTO> getLikedCountFromRedis(); }
Implementation class RedisServiceImpl.java
import com.solo.coderiver.user.dataobject.UserLike; import com.solo.coderiver.user.dto.LikedCountDTO; import com.solo.coderiver.user.enums.LikedStatusEnum; import com.solo.coderiver.user.service.LikedService; import com.solo.coderiver.user.service.RedisService; import com.solo.coderiver.user.utils.RedisKeyUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Map; @Service @Slf4j 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()); } @Override public void unlikeFromRedis(String likedUserId, String likedPostId) { String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId); redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode()); } @Override public void deleteLikedFromRedis(String likedUserId, String likedPostId) { String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId); redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key); } @Override public void incrementLikedCount(String likedUserId) { redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, 1); } @Override public void decrementLikedCount(String likedUserId) { redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -1); } @Override public List<UserLike> getLikedDataFromRedis() { Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE); List<UserLike> list = new ArrayList<>(); while (cursor.hasNext()){ Map.Entry<Object, Object> entry = cursor.next(); String key = (String) entry.getKey(); //Separate likedUserId,likedPostId String[] split = key.split("::"); String likedUserId = split[0]; String likedPostId = split[1]; Integer value = (Integer) entry.getValue(); //Assemble into UserLike object UserLike userLike = new UserLike(likedUserId, likedPostId, value); list.add(userLike); //Save to list After from Redis Delete in redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key); } return list; } @Override public List<LikedCountDTO> getLikedCountFromRedis() { Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE); List<LikedCountDTO> list = new ArrayList<>(); while (cursor.hasNext()){ Map.Entry<Object, Object> map = cursor.next(); //Store likes in LikedCountDT String key = (String)map.getKey(); LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue()); list.add(dto); //Delete this record from Redis redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key); } return list; } }
Tool classes and enumeration classes used
RedisKeyUtils is used to generate keys according to certain rules
public class RedisKeyUtils { //key to save user likes data public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED"; //key to save the number of users being liked public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT"; /** * Splice the user id of the likes and the id of the likes as the key. Format 222222:: 333333 * @param likedUserId Liked person id * @param likedPostId id of the person who likes * @return */ public static String getLikedKey(String likedUserId, String likedPostId){ StringBuilder builder = new StringBuilder(); builder.append(likedUserId); builder.append("::"); builder.append(likedPostId); return builder.toString(); } }
Enumeration class of LikedStatusEnum user likes status
package com.solo.coderiver.user.enums; import lombok.Getter; /** * Status of user likes */ @Getter public enum LikedStatusEnum { LIKE(1, "give the thumbs-up"), UNLIKE(0, "Cancel like/No likes"), ; private Integer code; private String msg; LikedStatusEnum(Integer code, String msg) { this.code = code; this.msg = msg; } }
I would like to recommend my own Spring Boot project:
https://github.com/YunaiV/ruoyi-vue-pro
2, Database design
The database table must contain at least three fields: like user id, like user id and like status. Add the primary key id, creation time and modification time.
Create table statement
create table `user_like`( `id` int not null auto_increment, `liked_user_id` varchar(32) not null comment 'Liked users id', `liked_post_id` varchar(32) not null comment 'Like users id', `status` tinyint(1) default '1' comment 'Like status, 0 cancel, 1 like', `create_time` timestamp not null default current_timestamp comment 'Creation time', `update_time` timestamp not null default current_timestamp on update current_timestamp comment 'Modification time', primary key(`id`), INDEX `liked_user_id`(`liked_user_id`), INDEX `liked_post_id`(`liked_post_id`) ) comment 'User likes table';
Corresponding object UserLike
import com.solo.coderiver.user.enums.LikedStatusEnum; import lombok.Data; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; /** * User likes table */ @Entity @Data public class UserLike { //Primary key id @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; //id of the user being liked private String likedUserId; //id of the user who likes private String likedPostId; //Like status. No like by default 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; } }
Recommend your own actual Spring Cloud project:
https://github.com/YunaiV/onemall
3, Database operation
The operation database is also encapsulated in the interface
LikedService
import com.solo.coderiver.user.dataobject.UserLike; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import java.util.List; public interface LikedService { /** * Save likes * @param userLike * @return */ UserLike save(UserLike userLike); /** * Batch save or modify * @param list */ List<UserLike> saveAll(List<UserLike> list); /** * Query the likes list according to the id of the person being liked (that is, query who has liked this person) * @param likedUserId id of the person being liked * @param pageable * @return */ Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable); /** * Query the like list according to the id of the person who likes it (that is, query who the person has liked it to) * @param likedPostId * @param pageable * @return */ Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable); /** * Query whether there is a like record through the like person and the like person id * @param likedUserId * @param likedPostId * @return */ UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId); /** * Store the likes data in Redis into the database */ void transLikedFromRedis2DB(); /** * Store the likes data in Redis into the database */ void transLikedCountFromRedis2DB(); }
LikedServiceImpl implementation class
import com.solo.coderiver.user.dataobject.UserInfo; import com.solo.coderiver.user.dataobject.UserLike; import com.solo.coderiver.user.dto.LikedCountDTO; import com.solo.coderiver.user.enums.LikedStatusEnum; import com.solo.coderiver.user.repository.UserLikeRepository; import com.solo.coderiver.user.service.LikedService; import com.solo.coderiver.user.service.RedisService; import com.solo.coderiver.user.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @Slf4j public class LikedServiceImpl implements LikedService { @Autowired UserLikeRepository likeRepository; @Autowired RedisService redisService; @Autowired UserService userService; @Override @Transactional public UserLike save(UserLike userLike) { return likeRepository.save(userLike); } @Override @Transactional public List<UserLike> saveAll(List<UserLike> list) { return likeRepository.saveAll(list); } @Override public Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable) { return likeRepository.findByLikedUserIdAndStatus(likedUserId, LikedStatusEnum.LIKE.getCode(), pageable); } @Override public Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable) { return likeRepository.findByLikedPostIdAndStatus(likedPostId, LikedStatusEnum.LIKE.getCode(), pageable); } @Override public UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId) { return likeRepository.findByLikedUserIdAndLikedPostId(likedUserId, likedPostId); } @Override @Transactional public void transLikedFromRedis2DB() { List<UserLike> list = redisService.getLikedDataFromRedis(); for (UserLike like : list) { UserLike ul = getByLikedUserIdAndLikedPostId(like.getLikedUserId(), like.getLikedPostId()); if (ul == null){ //No record, direct deposit save(like); }else{ //There are records and need to be updated ul.setStatus(like.getStatus()); save(ul); } } } @Override @Transactional public void transLikedCountFromRedis2DB() { List<LikedCountDTO> list = redisService.getLikedCountFromRedis(); for (LikedCountDTO dto : list) { UserInfo user = userService.findById(dto.getId()); //The number of likes is an insignificant operation. There is no need to throw an exception when an error occurs if (user != null){ Integer likeNum = user.getLikeNum() + dto.getCount(); user.setLikeNum(likeNum); //Update the number of likes userService.updateInfo(user); } } } }
These are the operations of the database, mainly adding, deleting, modifying and querying.
4, Enable scheduled task persistence storage to the database
Timing task Quartz is very powerful, so use it.
To use Quartz:
1. Add dependency
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency>
2. Prepare configuration file
package com.solo.coderiver.user.config; import com.solo.coderiver.user.task.LikeTask; import org.quartz.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.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() // . withIntervalInSeconds(10) // Sets the time period in seconds .withIntervalInHours(2) //Once every two hours .repeatForever(); return TriggerBuilder.newTrigger().forJob(quartzDetail()) .withIdentity(LIKE_TASK_IDENTITY) .withSchedule(scheduleBuilder) .build(); } }
3. Write the class to execute the task, which inherits from QuartzJobBean
package com.solo.coderiver.user.task; import com.solo.coderiver.user.service.LikedService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.time.DateUtils; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.QuartzJobBean; import java.text.SimpleDateFormat; import java.util.Date; /** * Timed tasks like */ @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 jobExecutionContext) throws JobExecutionException { log.info("LikeTask-------- {}", sdf.format(new Date())); //take Redis Sync the likes in the database likedService.transLikedFromRedis2DB(); likedService.transLikedCountFromRedis2DB(); } }
Directly call the method encapsulated by LikedService in the scheduled task to complete data synchronization.
The above is the design and implementation of the praise function. Please give more advice on the shortcomings.
In addition, like / cancel like and like number + 1 / - 1 should be atomic operations, otherwise there will be two duplicate like records in case of concurrency problems, Therefore, lock the entire atomic operation. At the same time, the process of synchronizing redis praise data to mysql needs to be supplemented in the system shutdown hook function of Spring Boot. Otherwise, the server may be updated 1 hour and 59 minutes from the last synchronization, It empties two hours of likes data. It's embarrassing if likes are designed for more important activities
If there is a better implementation scheme, welcome to communicate in the comment area
Click on the bottom card / WeChat search to pay attention to the official account of "ID:gh_cc865e4c536b" (Tian Yuwen).
It's said that those who praise and pay attention to this number have found a beautiful little sister, and they will enter a million years later!!