SpringBoot实现文章点赞

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

  • 用户点“点赞”按钮

  • redis存储这个“赞”

  • 用户取消“赞”

  • redis随之取消“赞”

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

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

redis缓存相关

  <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  1. 在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
    
  1. 定时任务相关
    一样的,引入定时的依赖:

           <!--定时任务-->
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-quartz</artifactId>
           </dependency>
    

    并配置:

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

    制定任务:

      ```java
      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();
          }
      }
    
  1. 数据库表结构的设计

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

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

最终的表,是这个样子的:


点赞表
  1. 具体实现业务逻辑

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

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

    @程序猿DD

    因为 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;
            }
        }
    }
}
  1. 用到的工具类
    对点赞信息进行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"
}

此时缓存中可见:

![image](https://upload-images.jianshu.io/upload_images/19860184-50ad4050f1d8cc3f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 

过了一会,缓存数据将被存进数据库中:缓存中没有数据,且值被写入数据库。

![image](https://upload-images.jianshu.io/upload_images/19860184-c0ad37bfc244af80.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 

至此,点赞完成!!!撒花★,°:.☆( ̄▽ ̄)/$:.°★

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容