SpringBoot實(shí)現(xiàn)文章點(diǎn)贊

點(diǎn)贊這種需求還算是很常見(jiàn)的季惩,其大體流程也是很容易想明白的邀摆。因?yàn)轭愃朴邳c(diǎn)贊這種操作签财,如果用戶比較閑,就是一頓點(diǎn)...點(diǎn)一下我就操作一下數(shù)據(jù)庫(kù)淹朋,取消一下我再操作一下數(shù)據(jù)庫(kù)......所以具體實(shí)現(xiàn)思路是:

  • 用戶點(diǎn)“點(diǎn)贊”按鈕

  • redis存儲(chǔ)這個(gè)“贊”

  • 用戶取消“贊”

  • redis隨之取消“贊”

  • 一定時(shí)間后笙各,系統(tǒng)將這些“贊”做持久化

思路是這樣的,具體實(shí)現(xiàn)也是比較容易的:

redis緩存相關(guān)

  <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  1. 在maven引入依賴后础芍,對(duì)redis進(jìn)行相關(guān)配置

    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. 定時(shí)任務(wù)相關(guān)
    一樣的杈抢,引入定時(shí)的依賴:

           <!--定時(shí)任務(wù)-->
           <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)  //設(shè)置時(shí)間周期單位秒,這樣效果更明顯
                       //.withIntervalInHours(2)  //兩個(gè)小時(shí)執(zhí)行一次
                       .repeatForever();
               return TriggerBuilder.newTrigger().forJob(quartzDetail())
                       .withIdentity(LIKE_TASK_IDENTITY)
                       .withSchedule(scheduleBuilder)
                       .build();
           }
       }
    

    制定任務(wù):

      ```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;
      
      /**
       * 點(diǎn)贊的定時(shí)任務(wù)
       */
      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 里的點(diǎn)贊信息同步到數(shù)據(jù)庫(kù)里
              likedService.transLikedFromRedis2DB();
              likedService.transLikedCountFromRedis2DB();
          }
      }
    
  1. 數(shù)據(jù)庫(kù)表結(jié)構(gòu)的設(shè)計(jì)

    因?yàn)椴┛晚?xiàng)目算是個(gè)小項(xiàng)目了,這里為了演示方便仑性,點(diǎn)贊這個(gè)模塊就先以簡(jiǎn)易為主惶楼。

liked_user_id為被贊者,liked_post_id為發(fā)出者诊杆。

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

/**
 * 用戶點(diǎn)贊表
 */

public class UserLike {

    //主鍵id
    private String likeId;

    //被點(diǎn)贊的用戶的id
    private String likedUserId;

    //點(diǎn)贊的用戶的id
    private String likedPostId;

    //點(diǎn)贊的狀態(tài).默認(rèn)未點(diǎn)贊
    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
}

其中歼捐,用了枚舉。

 /**
     * 用戶點(diǎn)贊的狀態(tài)
  */
public enum LikedStatusEnum {
    /**
     * 點(diǎn)贊
     */
    LIKE(1, "點(diǎn)贊"),
    /**
     * 取消贊
     */
    UNLIKE(0, "取消點(diǎn)贊/未點(diǎn)贊");

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

最終的表晨汹,是這個(gè)樣子的:


點(diǎn)贊表
  1. 具體實(shí)現(xiàn)業(yè)務(wù)邏輯

    這里有兩點(diǎn):第一是豹储,先把用戶的“贊”存在緩存層;第二宰缤,適當(dāng)?shù)臅r(shí)間颂翼,將緩存的數(shù)據(jù)拿出晃洒,進(jìn)行持久化操作。

    考慮到redis存儲(chǔ)的特點(diǎn)朦乏,選用hash的形式對(duì)“用戶點(diǎn)贊操作”及“用戶被點(diǎn)贊數(shù)量”兩項(xiàng)進(jìn)行存儲(chǔ)球及。采用hash的具體原因:把點(diǎn)贊造成的不同影響,儲(chǔ)存為不同分區(qū)呻疹,方便管理吃引。

    @程序猿DD

    因?yàn)?Hash 里的數(shù)據(jù)都是存在一個(gè)鍵里,可以通過(guò)這個(gè)鍵很方便的把所有的點(diǎn)贊數(shù)據(jù)都取出刽锤。

    這個(gè)鍵里面的數(shù)據(jù)還可以存成鍵值對(duì)的形式镊尺,方便存入點(diǎn)贊人、被點(diǎn)贊人和點(diǎn)贊狀態(tài)并思。

第一庐氮,先把用戶的“贊”存在緩存層。

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 對(duì)象
            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();
            //將點(diǎn)贊數(shù)量存儲(chǔ)在 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. 用到的工具類
    對(duì)點(diǎn)贊信息進(jìn)行redis儲(chǔ)存的id生成:
public class RedisKeyUtils {

    //保存用戶點(diǎn)贊數(shù)據(jù)的key
    public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED";
    //保存用戶被點(diǎn)贊數(shù)量的key
    public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT";

    /**
     * 拼接被點(diǎn)贊的用戶id和點(diǎn)贊的人的id作為key输涕。格式 222222::333333
     * @param likedUserId 被點(diǎn)贊的人id
     * @param likedPostId 點(diǎn)贊的人的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();
    }
}

因?yàn)橄胱鲆粋€(gè)分布式項(xiàng)目音婶,所以項(xiàng)目用到的id生成策略采用了雪花算法,代碼過(guò)長(zhǎng)莱坎,就不貼了衣式。
測(cè)試,給測(cè)試來(lái)個(gè)接口檐什,用postman測(cè)吧碴卧。

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

發(fā)送值為:

{
        "likedUserId":"123",
        "likedPostId":"456"
}

此時(shí)緩存中可見(jiàn):

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

過(guò)了一會(huì),緩存數(shù)據(jù)將被存進(jìn)數(shù)據(jù)庫(kù)中:緩存中沒(méi)有數(shù)據(jù)乃正,且值被寫入數(shù)據(jù)庫(kù)螟深。

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

至此,點(diǎn)贊完成L淘帷!凡蜻!撒花★,°:.☆( ̄▽ ̄)/$:.°★ 搭综。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市划栓,隨后出現(xiàn)的幾起案子兑巾,更是在濱河造成了極大的恐慌,老刑警劉巖忠荞,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蒋歌,死亡現(xiàn)場(chǎng)離奇詭異帅掘,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)堂油,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門修档,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人府框,你說(shuō)我怎么就攤上這事吱窝。” “怎么了迫靖?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵院峡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我系宜,道長(zhǎng)照激,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任盹牧,我火速辦了婚禮俩垃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘欢策。我一直安慰自己吆寨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布踩寇。 她就那樣靜靜地躺著啄清,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俺孙。 梳的紋絲不亂的頭發(fā)上辣卒,一...
    開(kāi)封第一講書(shū)人閱讀 52,328評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音睛榄,去河邊找鬼荣茫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛场靴,可吹牛的內(nèi)容都是我干的啡莉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼旨剥,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼咧欣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起轨帜,我...
    開(kāi)封第一講書(shū)人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤魄咕,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蚌父,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體哮兰,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡毛萌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了喝滞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阁将。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖囤躁,靈堂內(nèi)的尸體忽然破棺而出冀痕,到底是詐尸還是另有隱情,我是刑警寧澤狸演,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布言蛇,位于F島的核電站,受9級(jí)特大地震影響宵距,放射性物質(zhì)發(fā)生泄漏腊尚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一满哪、第九天 我趴在偏房一處隱蔽的房頂上張望婿斥。 院中可真熱鬧,春花似錦哨鸭、人聲如沸民宿。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)活鹰。三九已至,卻和暖如春只估,著一層夾襖步出監(jiān)牢的瞬間志群,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工蛔钙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锌云,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓吁脱,卻偏偏與公主長(zhǎng)得像桑涎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子兼贡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容