點贊
在我個人理解中,點贊業務比較頻繁,很多人業務可能都會有這個,比如:部落格,影片,文章,動態,評論等,但是不應該是核心業務,不應該大量地請求MySQL資料庫,給資料庫造成大量的資源消耗,MySQL的資料庫是非常寶貴的.
以某音為例,當我去搜尋的時候,全抖音比較高的點贊數目應該是在1200w - 2000w,我們自己的業務肯定答不到這麼高的,但是假設有10個100w的點讚的部落格,user_id為使用者ID,publication_id為部落格的id
-
第一種方式是直接運算元據庫.每次有點贊或者取消點贊操作時,就更新MySQL資料庫的點贊數.同時,插入或者更新一個user_id和publication_id的資料行,如果點贊操作非常頻繁,會對資料庫產生很大的壓力.如果有大量的點贊記錄,會對資料庫產生很大的資料量,一篇文章,100w+的點讚的記錄,對於MySQL來說,是非常恐怖的.
-
第二種方式是透過MySQL + Redis的Set來實現,具體程式碼如下,以下的程式碼為B站Redis黑馬點評專案:
@Override public Result likeBlog(Long id){ // 1. 獲取登入使用者 Long userId = UserHolder.getUser().getId(); // 2. 判斷當前登入使用者是否已經點贊 String key = BLOG_LIKED_KEY + id; Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString()); if(BooleanUtil.isFalse(isMember)){ // 3. 如果未點贊,可以點贊 // 3.1 資料庫點贊數+1 boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update(); // 3.2 儲存使用者到Redis的set集合 if(isSuccess){ stringRedisTemplate.opsForSet().add(key, userId.toString()); } } else { // 4. 如果已點贊,取消點贊 // 4.1 資料庫點贊數-1 boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update(); // 4.2 把使用者從Redis的set集合移除 if(isSuccess){ stringRedisTemplate.opsForSet().remove(key, userId.toString()); } } }
這樣造成的問題是,Redis是記憶體資料庫,點贊資訊儲存在記憶體中。當點贊數量非常大時,會佔用大量記憶體。
下面測試一下,一個key為"userId:114514:publication_id:738836",value為100000-1100000的資料
-
資料量
scard userId:114514:publication_id:738836
-
判斷一個value是否存在這個set中-----(對應的業務為"判斷當前登入使用者是否已經點贊")
@Test public void selectBigKey() { String key = "userId:114514:publication_id:738836"; String value1 = "100000"; String value2 = "5000000"; // 記錄開始時間 long startTime = System.nanoTime(); boolean cacheSet1 = RedisUtils.containsInCacheSet(key, value1); if (cacheSet1) { System.out.println("程式碼2:" + "存在這個value"); } else { System.out.println("程式碼2:" + "不存在這個value"); } // 記錄結束時間 long endTime = System.nanoTime(); // 計算執行時間(以毫秒為單位) long executionTime = (endTime - startTime) / 1_000_000; // 將納秒轉換為毫秒 System.out.println("程式碼執行時間1: " + executionTime + " 毫秒"); // 記錄開始時間 long startTime2 = System.nanoTime(); boolean cacheSet2 = RedisUtils.containsInCacheSet(key, value2); if (cacheSet2) { System.out.println("程式碼2:" + "存在這個value"); } else { System.out.println("程式碼2:" + "不存在這個value"); } // 記錄結束時間 long endTime2 = System.nanoTime(); // 計算執行時間(以毫秒為單位) long executionTime2 = (endTime2 - startTime2) / 1_000_000; // 將納秒轉換為毫秒 System.out.println("程式碼執行時間2: " + executionTime2 + " 毫秒"); }
可以看到,其實對於時間來說,61毫秒和66毫秒可以說時間非常短了,不愧是redis,即使資料量很大,但是查詢一個value是否在比較大的set的效率是非常短的.
-
往一個key中新增一個value,或者刪除一個value--->(對應一個點贊,和取消點贊)
@Test public void addAndRemoveElementFromBigKey() { String key = "userId:114514:publication_id:738836"; String value1 = "10000000"; String value2 = "200000"; // 記錄開始時間 long startTime = System.nanoTime(); boolean cacheSet1 = RedisUtils.addToCacheSet(key, value1); // 記錄結束時間 long endTime = System.nanoTime(); // 計算執行時間(以毫秒為單位) long executionTime = (endTime - startTime) / 1_000_000; // 將納秒轉換為毫秒 System.out.println("新增一個元素的執行時間: " + executionTime + " 毫秒"); // 記錄開始時間 long startTime2 = System.nanoTime(); boolean cacheSet2 = RedisUtils.removeFromCacheSet(key, value2); // 記錄結束時間 long endTime2 = System.nanoTime(); // 計算執行時間(以毫秒為單位) long executionTime2 = (endTime2 - startTime2) / 1_000_000; // 將納秒轉換為毫秒 System.out.println("刪除一個元素的程式碼執行時間: " + executionTime2 + " 毫秒"); }
但從時間來講,只有一個字:快
-
查詢佔用的記憶體的空間
MEMORY USAGE userId:114514:publication_id:738836
-
其實可以看到,大概是佔用66mb,如果使用者的id為雪花演算法的id,那可能佔用的記憶體100mb
以上來說,主要還是一個bigkey的問題,如果點讚的數量過大,佔用的記憶體過大,寶貴的記憶體不應該給這種業務.
-
自然而然,我們想到用非關係型資料庫,但是不要是基於記憶體的,我想到的是用MongoDB的方案
我們可以往MongoDB中插入一條這樣的資料:
db.collectionName.insertOne({ "id": "yourIdValue", "userId": yourUserIdValue, "type": yourTypeValue, "likedItemId": yourLikedItemIdValue, "createTime": new Date("yourCreateTimeValue") });
id 主鍵id,userId為使用者的ID,type為文章或者動態或者其他的型別,likedItemId為文章或者動態或者其他的型別的主鍵ID,createTime為點贊時間
在MongoDB中,可以使用
createIndex
方法來建立唯一索引。為userId,type和
likedItemId欄位建立一個唯一索引。db.collectionName.createIndex( { "userId": 1, "type": 1, "likedItemId": 1 }, { unique: true, name: "unique_index_name" } );
詳細解釋:
collectionName
:集合名稱。unique_index_name
:你想要給索引起的名字,可以根據你的需求替換為其他名稱。
這個命令將在
collectionName
集合上建立一個名為unique_index_name
的唯一索引,涵蓋了userId
、type
和likedItemId
欄位。1
表示升序,如果需要降序索引,可以使用-1
。unique: true
選項確保索引是唯一的。執行這個命令後,如果有重複的組合出現在這三個欄位上,MongoDB將會阻止插入並丟擲錯誤。
即如果裡面有記錄為已經點過贊,點贊就是往裡面加記錄,取消點贊就是刪除記錄
詳細程式碼如下:
@Service public class LikeServiceImpl implements LikeService { @Autowired private MongoTemplate mongoTemplate; @Autowired private PublicationService publicationService; /** * 為動態或者文章點贊 * * @param publicationId 動態或者文章的ID * @param userId 使用者的ID * @param type 型別,區分是文章還是動態 * @return 點贊總數 */ @Override public Integer likePublication(Long publicationId, Long userId, Integer type) { // 構建查詢條件 Criteria criteria = Criteria.where("userId").is(userId) .and("type").is(type) .and("likedItemId").is(publicationId); // 建立查詢物件並應用查詢條件 Query query = new Query(criteria); boolean isExists = mongoTemplate.exists(query, PublicationLike.class); if (isExists) { Asserts.fail("重複點贊"); } //將點贊記錄儲存到mongodb PublicationLike publicationLike = new PublicationLike(); publicationLike.setType(type); publicationLike.setCreateTime(DateUtil.date()); publicationLike.setLikedItemId(publicationId); publicationLike.setUserId(userId); PublicationLike savedLike = mongoTemplate.save(publicationLike); //點贊數統計 String redisLikeCountKey = String.format(RedisConstant.PUBLICATION_LIKE_COUNT, publicationId, userId, type); Long likeCount = RedisUtils.getAtomicValueWithDefault(redisLikeCountKey); //如果沒有快取過點贊數,則查詢資料庫 if (likeCount.equals(-1L)) { Publication publication = publicationService.getById(publicationId); RedisUtils.setAtomicValue(redisLikeCountKey, publication.getLikeCount()); return publication.getLikeCount(); } else { //返回點贊數+1 return Math.toIntExact(RedisUtils.incrAtomicValue(redisLikeCountKey)); } } @Override public Integer unlikePublication(Long publicationId, Long userId, Integer type) { // 構建查詢條件 Criteria criteria = Criteria.where("userId").is(userId) .and("type").is(type) .and("likedItemId").is(publicationId); // 建立查詢物件並應用查詢條件 Query query = new Query(criteria); boolean isExists = mongoTemplate.exists(query, PublicationLike.class); if (!isExists) { Asserts.fail("未點贊過該內容,無法取消點贊"); } // 從MongoDB中刪除點贊記錄 mongoTemplate.remove(query, PublicationLike.class); // 更新點贊數統計 String redisLikeCountKey = String.format(RedisConstant.PUBLICATION_LIKE_COUNT, publicationId, userId, type); Long likeCount = RedisUtils.getAtomicValueWithDefault(redisLikeCountKey); // 如果點贊數存在於快取中,減少點贊數並返回 if (!likeCount.equals(-1L)) { long newLikeCount = RedisUtils.decrAtomicValue(redisLikeCountKey); return Math.toIntExact(newLikeCount); } else { // 如果點贊數沒有快取,查詢資料庫並更新快取 Publication publication = publicationService.getById(publicationId); if (publication != null) { RedisUtils.setAtomicValue(redisLikeCountKey, publication.getLikeCount()); return publication.getLikeCount(); } else { Asserts.fail("無法獲取點贊數"); return 0; } } } }