Redis實現(xiàn)點贊功能模塊

之前看了一篇文章,講redis的應(yīng)用場景,其中一個應(yīng)用場景就是實現(xiàn)點贊功能,紙上得來恐覺淺,必須實戰(zhàn)一波

功能點設(shè)計

比如我喜歡發(fā)文章的掘金網(wǎng)站就有點贊的功能,統(tǒng)計文章點贊的總數(shù),用戶所有文章的點贊數(shù),因此設(shè)計的點贊功能模塊具有以下功能點:

  • 某篇文章的點贊數(shù)
  • 用戶所有文章的點贊數(shù)
  • 用戶點贊的文章
  • 持久化到MySQL數(shù)據(jù)庫

數(shù)據(jù)庫設(shè)計

  • Redis數(shù)據(jù)庫設(shè)計
    RedisK-V數(shù)據(jù)庫,沒有統(tǒng)一的數(shù)據(jù)結(jié)構(gòu),針對不同的功能點設(shè)計了不同的K-V存儲結(jié)構(gòu)

    • 用戶某篇文章的點贊數(shù)
      使用HashMap數(shù)據(jù)結(jié)構(gòu),HashMap中的keyarticleId,valueSetSet中的值為用戶ID,即HashMap<String, Set<String>>
    • 用戶總的點贊數(shù)
      使用HashMap數(shù)據(jù)結(jié)構(gòu),HashMap中的keyuserId,valueString記錄總的點贊數(shù)
    • 用戶點贊的文章
      使用HashMap數(shù)據(jù)結(jié)構(gòu),HashMap中的keyuserId,valueSet,Set中的值為文章ID,即HashMap<String, Set<String>>
  • MySQL數(shù)據(jù)庫設(shè)計
    最主要的兩張表,article表和user_like_article

    • article表結(jié)構(gòu)
    字段值 字段類型 說明
    article_name varchar 文章名字
    content blob 文章內(nèi)容
    total_like_count bigint 文章總點贊數(shù)

    文章總的點贊數(shù)需要和Redis中的點贊數(shù)進(jìn)行同步

    • user_like_article表結(jié)構(gòu)
    字段值 字段類型 說明
    user_id bigint 用戶ID
    article_id bigint 文章ID

    記錄用戶點贊文章的信息,是一張中間表

說明:表結(jié)構(gòu)設(shè)計省略了id、deletedgmt_create、gmt_modified字段

流程圖

流程圖

流程圖比較簡單,點贊和取消點贊基本實現(xiàn)步驟相同

  • 參數(shù)校驗
    對傳入的參數(shù)進(jìn)行null值判斷
  • 邏輯校驗
    對于用戶點贊,用戶不能重復(fù)點贊相同的文章
    對于取消點贊,用戶不能取消未點贊的文章
  • 存入Redis
    存入的數(shù)據(jù)主要有所有文章的點贊數(shù)、某篇文章的點贊數(shù)、用戶點贊的文章
  • 定時任務(wù)
    通過定時【1小時執(zhí)行一次】,從Redis讀取數(shù)據(jù)持久化到MySQL

代碼功能實現(xiàn)

  • 點贊
public void likeArticle(Long articleId, Long likedUserId, Long likedPostId) {
    validateParam(articleId, likedUserId, likedPostId);  //參數(shù)驗證

    logger.info("點贊數(shù)據(jù)存入redis開始,articleId:{},likedUserId:{},likedPostId:{}", articleId, likedUserId, likedPostId);
    synchronized (this) {
        //只有未點贊的用戶才可以進(jìn)行點贊
        likeArticleLogicValidate(articleId, likedUserId, likedPostId);
        //1.用戶總點贊數(shù)+1
        redisTemplate.opsForHash().increment(TOTAL_LIKE_COUNT_KEY, String.valueOf(likedUserId), 1);

        //2.用戶喜歡的文章+1
        String userLikeResult = (String) redisTemplate.opsForHash().get(USER_LIKE_ARTICLE_KEY, String.valueOf(likedPostId));
        Set<Long> articleIdSet = userLikeResult == null ? new HashSet<>() : FastjsonUtil.deserializeToSet(userLikeResult, Long.class);
            articleIdSet.add(articleId);
        redisTemplate.opsForHash().put(USER_LIKE_ARTICLE_KEY, String.valueOf(likedPostId), FastjsonUtil.serialize(articleIdSet));

        //3.文章點贊數(shù)+1
        String articleLikedResult = (String) redisTemplate.opsForHash().get(ARTICLE_LIKED_USER_KEY, String.valueOf(articleId));
        Set<Long> likePostIdSet = articleLikedResult == null ? new HashSet<>() : FastjsonUtil.deserializeToSet(articleLikedResult, Long.class);
        likePostIdSet.add(likedPostId);
        redisTemplate.opsForHash().put(ARTICLE_LIKED_USER_KEY, String.valueOf(articleId), FastjsonUtil.serialize(likePostIdSet));
        logger.info("取消點贊數(shù)據(jù)存入redis結(jié)束,articleId:{},likedUserId:{},likedPostId:{}", articleId, likedUserId, likedPostId);
    }
}
  • 取消點贊
public void unlikeArticle(Long articleId, Long likedUserId, Long likedPostId) {
    validateParam(articleId, likedUserId, likedPostId);  //參數(shù)校驗

    logger.info("取消點贊數(shù)據(jù)存入redis開始,articleId:{},likedUserId:{},likedPostId:{}", articleId, likedUserId, likedPostId);
    //1.用戶總點贊數(shù)-1
    synchronized (this) {
        //只有點贊的用戶才可以取消點贊
        unlikeArticleLogicValidate(articleId, likedUserId, likedPostId);
        Long totalLikeCount = Long.parseLong((String)redisTemplate.opsForHash().get(TOTAL_LIKE_COUNT_KEY, String.valueOf(likedUserId)));
         redisTemplate.opsForHash().put(TOTAL_LIKE_COUNT_KEY, String.valueOf(likedUserId), String.valueOf(--totalLikeCount));

        //2.用戶喜歡的文章-1
        String userLikeResult = (String) redisTemplate.opsForHash().get(USER_LIKE_ARTICLE_KEY, String.valueOf(likedPostId));
        Set<Long> articleIdSet = FastjsonUtil.deserializeToSet(userLikeResult, Long.class);
        articleIdSet.remove(articleId);
        redisTemplate.opsForHash().put(USER_LIKE_ARTICLE_KEY, String.valueOf(likedPostId), FastjsonUtil.serialize(articleIdSet));

        //3.取消用戶某篇文章的點贊數(shù)
        String articleLikedResult = (String) redisTemplate.opsForHash().get(ARTICLE_LIKED_USER_KEY, String.valueOf(articleId));
        Set<Long> likePostIdSet = FastjsonUtil.deserializeToSet(articleLikedResult, Long.class);
        likePostIdSet.remove(likedPostId);
        redisTemplate.opsForHash().put(ARTICLE_LIKED_USER_KEY, String.valueOf(articleId), FastjsonUtil.serialize(likePostIdSet));
    }

    logger.info("取消點贊數(shù)據(jù)存入redis結(jié)束,articleId:{},likedUserId:{},likedPostId:{}", articleId, likedUserId, likedPostId);
}
  • 異步落庫
@Scheduled(cron = "0 0 0/1 * * ? ")
public void redisDataToMySQL() {
    logger.info("time:{},開始執(zhí)行Redis數(shù)據(jù)持久化到MySQL任務(wù)", LocalDateTime.now().format(formatter));
    //1.更新文章總的點贊數(shù)
    Map<String, String> articleCountMap = redisTemplate.opsForHash().entries(ARTICLE_LIKED_USER_KEY);
    for (Map.Entry<String, String> entry : articleCountMap.entrySet()) {
        String articleId = entry.getKey();
        Set<Long> userIdSet = FastjsonUtil.deserializeToSet(entry.getValue(), Long.class);
        //1.同步某篇文章總的點贊數(shù)到MySQL
        synchronizeTotalLikeCount(articleId, userIdSet);
        //2.同步用戶喜歡的文章
        synchronizeUserLikeArticle(articleId, userIdSet);
    }
    logger.info("time:{},結(jié)束執(zhí)行Redis數(shù)據(jù)持久化到MySQL任務(wù)", LocalDateTime.now().format(formatter));
}

說明:

  • 針對存在并發(fā)的問題,通過添加synchronize關(guān)鍵字實現(xiàn)
  • 另外還有獲取某篇文章的點贊數(shù)、用戶所有文章的點贊數(shù)、用戶點贊的文章方法實現(xiàn),方法實現(xiàn)比較簡單不說明,可以在完整代碼中找到

目前存在的不足

  • 用戶點贊\取消點贊方法中,Redis事務(wù)沒有保證
  • 該應(yīng)用只適用于單機環(huán)境,分布式環(huán)境下存在并發(fā)問題,分布式鎖待完成

十一過后對假期意猶未盡

最后附:完整代碼地址
歡迎forkstar,如有紕漏歡迎指正

附往期文章:歡迎你的閱讀、點贊、評論

并發(fā)相關(guān):
1.為什么阿里巴巴要禁用Executors創(chuàng)建線程池?
2.自己的事情自己做,線程異常處理
設(shè)計模式相關(guān):
1. 單例模式,你真的寫對了嗎?
2. (策略模式+工廠模式+map)套餐 Kill 項目中的switch case

JAVA8相關(guān):
1. 使用Stream API優(yōu)化代碼
2. 親,建議你使用LocalDateTime而不是Date哦

數(shù)據(jù)庫相關(guān):
1. mysql數(shù)據(jù)庫時間類型datetime、bigint、timestamp的查詢效率比較
2. 很高興!終于踩到了慢查詢的坑

高效相關(guān):
1. 擼一個Java腳手架,一統(tǒng)團(tuán)隊項目結(jié)構(gòu)風(fēng)格

日志相關(guān):
1. 日志框架,選擇Logback Or Log4j2?
2. Logback配置文件這么寫,TPS提高10倍

工程相關(guān):
1. 閑來無事,動手寫一個LRU本地緩存
2. JMX可視化監(jiān)控線程池
3. 權(quán)限管理 【SpringSecurity篇】
4. Spring自定義注解從入門到精通
5. java模擬登陸優(yōu)酷
6. QPS這么高,那就來寫個多級緩存吧
7. java使用phantomjs進(jìn)行截圖

其他:
1. 使用try-with-resources優(yōu)雅關(guān)閉資源
2. 老板,用float存儲金額為什么要扣我工資

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容