SpringBoot实现文章点赞(二)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

点赞这种需求还算是很常见的其大体流程也是很容易想明白的。因为类似于点赞这种操作如果用户比较闲就是一顿点…点一下我就操作一下数据库取消一下我再操作一下数据库…所以具体实现思路是

用户点“点赞”按钮

redis存储这个“赞”

用户取消“赞”

redis随之取消“赞”

一定时间后系统将这些“赞”做持久化

思路是这样的具体实现也是比较容易的

redis缓存相关

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

在maven引入依赖后对redis进行相关配置

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

配置文件也要写一下

spring.redis.host=127.0.0.1
spring.redis.port: 6379

定时任务相关
一样的引入定时的依赖

org.springframework.boot spring-boot-starter-quartz
   import com.hanor.blog.quartz.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(60)  //设置时间周期单位秒,这样效果更明显
                   //.withIntervalInHours(2)  //两个小时执行一次
                   .repeatForever();
           return TriggerBuilder.newTrigger().forJob(quartzDetail())
                   .withIdentity(LIKE_TASK_IDENTITY)
                   .withSchedule(scheduleBuilder)
                   .build();
       }
   }

制定任务


  import com.hanor.blog.service.LikedService;
  import org.quartz.JobExecutionContext;
  import org.quartz.JobExecutionException;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.scheduling.quartz.QuartzJobBean;
  import org.springframework.stereotype.Component;
  
  import java.text.SimpleDateFormat;
  
  /**
   * 点赞的定时任务
   */
  public class LikeTask extends QuartzJobBean {
  
      @Autowired
      private LikedService likedService;
  
      private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  
      @Override
      protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
  
          System.out.println("-----------quartz------------");
          //将 Redis 里的点赞信息同步到数据库里
          likedService.transLikedFromRedis2DB();
          likedService.transLikedCountFromRedis2DB();
      }
  }

数据库表结构的设计

因为博客项目算是个小项目了这里为了演示方便点赞这个模块就先以简易为主。

liked_user_id为被赞者liked_post_id为发出者。

import com.hanor.blog.entity.enums.LikedStatusEnum;

/**
 * 用户点赞表
 */

public class UserLike {

    //主键id
    private String likeId;

    //被点赞的用户的id
    private String likedUserId;

    //点赞的用户的id
    private String likedPostId;

    //点赞的状态.默认未点赞
    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;
    }
    //getter setter
}

其中用了枚举。

 /**
     * 用户点赞的状态
  */
public enum LikedStatusEnum {
    /**
     * 点赞
     */
    LIKE(1, "点赞"),
    /**
     * 取消赞
     */
    UNLIKE(0, "取消点赞/未点赞");

    private Integer code;

    private String msg;

    LikedStatusEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public Integer getCode(){
        return this.code;
    }

    public String getMsg(){
        return this.msg;
    }
}

具体实现业务逻辑

这里有两点第一是先把用户的“赞”存在缓存层第二适当的时间将缓存的数据拿出进行持久化操作。

考虑到redis存储的特点选用hash的形式对“用户点赞操作”及“用户被点赞数量”两项进行存储。采用hash的具体原因把点赞造成的不同影响储存为不同分区方便管理。

因为 Hash 里的数据都是存在一个键里可以通过这个键很方便的把所有的点赞数据都取出。

这个键里面的数据还可以存成键值对的形式方便存入点赞人、被点赞人和点赞状态。

第一先把用户的“赞”存在缓存层。

import com.hanor.blog.entity.DTO.LikedCountDTO;
import com.hanor.blog.entity.enums.LikedStatusEnum;
import com.hanor.blog.entity.pojo.UserLike;
import com.hanor.blog.service.RedisService;
import com.hanor.blog.util.RedisKeyUtils;
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
public class RedisServiceImpl implements RedisService {

    @Autowired
    RedisTemplate redisTemplate;

    @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();
            //分离出 likedUserIdlikedPostId
            String[] split = key.split("::");
            String likedUserId = split[0];
            String likedPostId = split[1];
            Integer value = (Integer) entry.getValue();

            //组装成 UserLike 对象
            UserLike userLike = new UserLike(likedUserId, likedPostId, value);
            list.add(userLike);

            //存到 list 后从 Redis 中删除
            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();
            //将点赞数量存储在 LikedCountDT
            String key = (String)map.getKey();
            LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue());
            list.add(dto);
            //从Redis中删除这条记录
            redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);
        }
        return list;
    }
}

第二持久化操作。

import com.alibaba.fastjson.JSONObject;
import com.hanor.blog.dao.BlogArticleMapper;
import com.hanor.blog.dao.UserLikeMapper;
import com.hanor.blog.entity.DTO.LikedCountDTO;
import com.hanor.blog.entity.pojo.BlogArticle;
import com.hanor.blog.entity.pojo.UserLike;
import com.hanor.blog.service.LikedService;
import com.hanor.blog.service.RedisService;
import com.hanor.blog.util.IdUtil;
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 java.util.Date;
import java.util.List;
@Service
public class LikedServiceImpl implements LikedService {

    @Autowired
    private RedisService redisService;
    @Autowired
    private UserLikeMapper userLikeMapper;
    @Autowired
    private BlogArticleMapper blogArticleMapper;
 
    @Override
    public int save(UserLike userLike) {
        return userLikeMapper.saveLike(userLike);
    }

    @Override
    public void saveAll(List<UserLike> list) {
        for (UserLike userLike : list) {
            userLikeMapper.saveLike(userLike);
        }
    }

    @Override
    public Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable) {
        return null;
    }

    @Override
    public Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable) {
        return null;
    }

    @Override
    public int getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId) {
        UserLike userLike = new UserLike();
        userLike.setLikedPostId(likedPostId);
        userLike.setLikedUserId(likedUserId);
        return userLikeMapper.searchLike(userLike);
    }

    @Override
    public void transLikedFromRedis2DB() {
        List<UserLike> userLikeList = redisService.getLikedDataFromRedis();
        for (UserLike like : userLikeList) {
            Integer userLikeExist = userLikeMapper.searchLike(like);
            if (userLikeExist > 0){
                userLikeMapper.updateLike(like);
            }else {
                like.setLikeId(IdUtil.nextId() + "");
                userLikeMapper.saveLike(like);
            }
        }
    }

    @Override
    public void transLikedCountFromRedis2DB() {
        List<LikedCountDTO> likedCountDTOs = redisService.getLikedCountFromRedis();

        for (LikedCountDTO dto : likedCountDTOs) {
            JSONObject blogArticle = blogArticleMapper.getArticleById(dto.getUserId());
            if (null != blogArticle){
                BlogArticle article = new BlogArticle();
                article.setUpdateTime(new Date());
                article.setArticleId(blogArticle.getString("articleId"));
                article.setArticleLike(blogArticle.getInteger("articleLike") + dto.getLikedNum());
                blogArticleMapper.updateArticle(article);
            }else {
                return;
            }
        }
    }
}

用到的工具类
对点赞信息进行redis储存的id生成

public class RedisKeyUtils {

    //保存用户点赞数据的key
    public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED";
    //保存用户被点赞数量的key
    public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT";

    /**
     * 拼接被点赞的用户id和点赞的人的id作为key。格式 222222::333333
     * @param likedUserId 被点赞的人id
     * @param likedPostId 点赞的人的id
     * @return
     */
    public static String getLikedKey(String likedUserId, String likedPostId){
        StringBuilder builder = new StringBuilder();
        builder.append(likedUserId);
        builder.append("::");
        builder.append(likedPostId);
        return builder.toString();
    }
}

因为想做一个分布式项目所以项目用到的id生成策略采用了雪花算法代码过长就不贴了。
测试给测试来个接口用postman测吧。

import com.hanor.blog.entity.pojo.UserLike;
import com.hanor.blog.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/like")
public class LikeController {

    @Autowired
    private RedisService redisService;

    @PostMapping
    public void doLike(@RequestBody UserLike userLike){
        redisService.saveLiked2Redis(userLike.getLikedUserId(),userLike.getLikedPostId());
        redisService.incrementLikedCount(userLike.getLikedPostId());
    }
发送值为

{
        "likedUserId":"123",
        "likedPostId":"456"
}
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Spring