之前看了一篇文章,講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è)計
Redis是K-V數(shù)據(jù)庫,沒有統(tǒng)一的數(shù)據(jù)結(jié)構(gòu),針對不同的功能點設(shè)計了不同的K-V存儲結(jié)構(gòu)- 用戶某篇文章的點贊數(shù)
使用HashMap數(shù)據(jù)結(jié)構(gòu),HashMap中的key為articleId,value為Set,Set中的值為用戶ID,即HashMap<String, Set<String>> - 用戶總的點贊數(shù)
使用HashMap數(shù)據(jù)結(jié)構(gòu),HashMap中的key為userId,value為String記錄總的點贊數(shù) - 用戶點贊的文章
使用HashMap數(shù)據(jù)結(jié)構(gòu),HashMap中的key為userId,value為Set,Set中的值為文章ID,即HashMap<String, Set<String>>
- 用戶某篇文章的點贊數(shù)
-
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、deleted、gmt_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ā)問題,分布式鎖待完成
十一過后對假期意猶未盡

最后附:完整代碼地址
歡迎fork與 star,如有紕漏歡迎指正
附往期文章:歡迎你的閱讀、點贊、評論
并發(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存儲金額為什么要扣我工資