SpringBoot实现文章点赞(二)

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

用户点“点赞”按钮

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();
            //分离出 likedUserId,likedPostId
            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"
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

princeAladdin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值