redis implements the function of like and cancel like

Posted by gabo on Thu, 03 Feb 2022 12:54:13 +0100

1.2 integration of redis and SpringBoot projects

In POM Introducing dependencies into XML

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Add comment @ EnableCaching on 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);
    }
}

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, the configuration of Redis in the SpringBoot project has been completed 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 these 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 data of the likes, likes and likes, and the other is to simply count how many times each user has been liked.

Due to the need to record the likes and the likes, as well as the like status (like, cancel like), we also need 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, and the id of the person who is liked is likedUserId. When you like, the status is 1, and the status of canceling likes is 0. Take the id of the person who likes and the id of the person who is liked as keys, separate the two IDS with::, and take the like status 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 taking 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 and 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);

            //Delete from Redis after saving to list
            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 the number of 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 and the id of the person you like 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();
    }
}

LikedStatusEnum enumeration class of user likes

package com.solo.coderiver.user.enums;

import lombok.Getter;

/**
 * User likes status
 */
@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;
    }
}

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 likes 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;
    }
}

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 likes 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 likes record through the likes and likes 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, and there is no need to throw exceptions if there is an error
            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 and store to database
Quartz is very powerful, so I use it.

To use Quartz:

1. Add dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2. Write 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) / / set 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. The class that writes the execution task 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 task of liking
 */
@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()));

        //Synchronize the likes in Redis to 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, so the whole atomic operation should be locked

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 at 1 hour and 59 minutes from the last synchronization, and the whole two hours of likes data will be cleared It would be very embarrassing if you like to design more important business activities.

Topics: Redis