How does Redis efficiently implement the like and cancel like functions

Keywords: Java Redis Spring Boot Spring Cloud

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

Posted by eerikk2 on Tue, 30 Nov 2021 07:25:17 -0800