之前看了一篇文章,講redis的應用場景,其中一個應用場景就是實現點贊功能,紙上得來恐覺淺,必須實戰一波
功能點設計
比如我喜歡發文章的掘金網站就有點贊的功能,統計文章點贊的總數,用戶所有文章的點贊數,因此設計的點贊功能模塊具有以下功能點:
- 某篇文章的點贊數
- 用戶所有文章的點贊數
- 用戶點贊的文章
- 持久化到
MySQL
數據庫
數據庫設計
-
Redis數據庫設計
Redis
是K-V
數據庫,沒有統一的數據結構,針對不同的功能點設計了不同的K-V
存儲結構- 用戶某篇文章的點贊數
使用HashMap
數據結構,HashMap
中的key
為articleId
,value
為Set
,Set
中的值為用戶ID
,即HashMap<String, Set<String>>
- 用戶總的點贊數
使用HashMap
數據結構,HashMap
中的key
為userId
,value
為String
記錄總的點贊數 - 用戶點贊的文章
使用HashMap
數據結構,HashMap
中的key
為userId
,value
為Set
,Set
中的值為文章ID
,即HashMap<String, Set<String>>
- 用戶某篇文章的點贊數
-
MySQL數據庫設計
最主要的兩張表,article
表和user_like_article
-
article
表結構
字段值 字段類型 說明 article_name varchar 文章名字 content blob 文章內容 total_like_count bigint 文章總點贊數 文章總的點贊數需要和
Redis
中的點贊數進行同步-
user_like_article
表結構
字段值 字段類型 說明 user_id bigint 用戶ID article_id bigint 文章ID 記錄用戶點贊文章的信息,是一張中間表
-
說明:表結構設計省略了id
、deleted
、gmt_create
、gmt_modified
字段
流程圖
流程圖比較簡單,點贊和取消點贊基本實現步驟相同
- 參數校驗
對傳入的參數進行null
值判斷 - 邏輯校驗
對于用戶點贊,用戶不能重復點贊相同的文章
對于取消點贊,用戶不能取消未點贊的文章 - 存入
Redis
存入的數據主要有所有文章的點贊數、某篇文章的點贊數、用戶點贊的文章 - 定時任務
通過定時【1小時執行一次】,從Redis
讀取數據持久化到MySQL
中
代碼功能實現
- 點贊
public void likeArticle(Long articleId, Long likedUserId, Long likedPostId) {
validateParam(articleId, likedUserId, likedPostId); //參數驗證
logger.info("點贊數據存入redis開始,articleId:{},likedUserId:{},likedPostId:{}", articleId, likedUserId, likedPostId);
synchronized (this) {
//只有未點贊的用戶才可以進行點贊
likeArticleLogicValidate(articleId, likedUserId, likedPostId);
//1.用戶總點贊數+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.文章點贊數+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("取消點贊數據存入redis結束,articleId:{},likedUserId:{},likedPostId:{}", articleId, likedUserId, likedPostId);
}
}
- 取消點贊
public void unlikeArticle(Long articleId, Long likedUserId, Long likedPostId) {
validateParam(articleId, likedUserId, likedPostId); //參數校驗
logger.info("取消點贊數據存入redis開始,articleId:{},likedUserId:{},likedPostId:{}", articleId, likedUserId, likedPostId);
//1.用戶總點贊數-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.取消用戶某篇文章的點贊數
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("取消點贊數據存入redis結束,articleId:{},likedUserId:{},likedPostId:{}", articleId, likedUserId, likedPostId);
}
- 異步落庫
@Scheduled(cron = "0 0 0/1 * * ? ")
public void redisDataToMySQL() {
logger.info("time:{},開始執行Redis數據持久化到MySQL任務", LocalDateTime.now().format(formatter));
//1.更新文章總的點贊數
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.同步某篇文章總的點贊數到MySQL
synchronizeTotalLikeCount(articleId, userIdSet);
//2.同步用戶喜歡的文章
synchronizeUserLikeArticle(articleId, userIdSet);
}
logger.info("time:{},結束執行Redis數據持久化到MySQL任務", LocalDateTime.now().format(formatter));
}
說明:
- 針對存在并發的問題,通過添加
synchronize
關鍵字實現 - 另外還有獲取某篇文章的點贊數、用戶所有文章的點贊數、用戶點贊的文章方法實現,方法實現比較簡單不說明,可以在完整代碼中找到
目前存在的不足
- 用戶點贊\取消點贊方法中,
Redis
事務沒有保證 - 該應用只適用于單機環境,分布式環境下存在并發問題,分布式鎖待完成
十一過后對假期意猶未盡
最后附:完整代碼地址
歡迎fork與 star,如有紕漏歡迎指正
附往期文章:歡迎你的閱讀、點贊、評論
并發相關:
1.為什么阿里巴巴要禁用Executors創建線程池?
2.自己的事情自己做,線程異常處理
設計模式相關:
1. 單例模式,你真的寫對了嗎?
2. (策略模式+工廠模式+map)套餐 Kill 項目中的switch case
JAVA8相關:
1. 使用Stream API優化代碼
2. 親,建議你使用LocalDateTime而不是Date哦
數據庫相關:
1. mysql數據庫時間類型datetime、bigint、timestamp的查詢效率比較
2. 很高興!終于踩到了慢查詢的坑
高效相關:
1. 擼一個Java腳手架,一統團隊項目結構風格
日志相關:
1. 日志框架,選擇Logback Or Log4j2?
2. Logback配置文件這么寫,TPS提高10倍
工程相關:
1. 閑來無事,動手寫一個LRU本地緩存
2. JMX可視化監控線程池
3. 權限管理 【SpringSecurity篇】
4. Spring自定義注解從入門到精通
5. java模擬登陸優酷
6. QPS這么高,那就來寫個多級緩存吧
7. java使用phantomjs進行截圖