关于点赞业务对MySQL和Redis和MongoDB的思考
點(diǎn)贊
? 在我個人理解中,點(diǎn)贊業(yè)務(wù)比較頻繁,很多人業(yè)務(wù)可能都會有這個,比如:博客,視頻,文章,動態(tài),評論等,但是不應(yīng)該是核心業(yè)務(wù),不應(yīng)該大量地請求MySQL數(shù)據(jù)庫,給數(shù)據(jù)庫造成大量的資源消耗,MySQL的數(shù)據(jù)庫是非常寶貴的.
以某音為例,當(dāng)我去搜索的時候,全抖音比較高的點(diǎn)贊數(shù)目應(yīng)該是在1200w - 2000w,我們自己的業(yè)務(wù)肯定答不到這么高的,但是假設(shè)有10個100w的點(diǎn)贊的博客,user_id為用戶ID,publication_id為博客的id
-
第一種方式是直接操作數(shù)據(jù)庫.每次有點(diǎn)贊或者取消點(diǎn)贊操作時,就更新MySQL數(shù)據(jù)庫的點(diǎn)贊數(shù).同時,插入或者更新一個user_id和publication_id的數(shù)據(jù)行,如果點(diǎn)贊操作非常頻繁,會對數(shù)據(jù)庫產(chǎn)生很大的壓力.如果有大量的點(diǎn)贊記錄,會對數(shù)據(jù)庫產(chǎn)生很大的數(shù)據(jù)量,一篇文章,100w+的點(diǎn)贊的記錄,對于MySQL來說,是非常恐怖的.
-
第二種方式是通過MySQL + Redis的Set來實(shí)現(xiàn),具體代碼如下,以下的代碼為B站Redis黑馬點(diǎn)評項目:
@Override public Result likeBlog(Long id){ // 1. 獲取登錄用戶 Long userId = UserHolder.getUser().getId(); // 2. 判斷當(dāng)前登錄用戶是否已經(jīng)點(diǎn)贊 String key = BLOG_LIKED_KEY + id; Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString()); if(BooleanUtil.isFalse(isMember)){ // 3. 如果未點(diǎn)贊,可以點(diǎn)贊 // 3.1 數(shù)據(jù)庫點(diǎn)贊數(shù)+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. 如果已點(diǎn)贊,取消點(diǎn)贊 // 4.1 數(shù)據(jù)庫點(diǎn)贊數(shù)-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是內(nèi)存數(shù)據(jù)庫,點(diǎn)贊信息存儲在內(nèi)存中。當(dāng)點(diǎn)贊數(shù)量非常大時,會占用大量內(nèi)存。
下面測試一下,一個key為"userId:114514:publication_id:738836",value為100000-1100000的數(shù)據(jù)
-
數(shù)據(jù)量
scard userId:114514:publication_id:738836 -
判斷一個value是否存在這個set中-----(對應(yīng)的業(yè)務(wù)為"判斷當(dāng)前登錄用戶是否已經(jīng)點(diǎn)贊")
@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"); } // 記錄結(jié)束時間 long endTime = System.nanoTime(); // 計算執(zhí)行時間(以毫秒為單位) long executionTime = (endTime - startTime) / 1_000_000; // 將納秒轉(zhuǎn)換為毫秒 System.out.println("代碼執(zhí)行時間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"); } // 記錄結(jié)束時間 long endTime2 = System.nanoTime(); // 計算執(zhí)行時間(以毫秒為單位) long executionTime2 = (endTime2 - startTime2) / 1_000_000; // 將納秒轉(zhuǎn)換為毫秒 System.out.println("代碼執(zhí)行時間2: " + executionTime2 + " 毫秒"); }可以看到,其實(shí)對于時間來說,61毫秒和66毫秒可以說時間非常短了,不愧是redis,即使數(shù)據(jù)量很大,但是查詢一個value是否在比較大的set的效率是非常短的.
-
往一個key中添加一個value,或者刪除一個value--->(對應(yīng)一個點(diǎn)贊,和取消點(diǎn)贊)
@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); // 記錄結(jié)束時間 long endTime = System.nanoTime(); // 計算執(zhí)行時間(以毫秒為單位) long executionTime = (endTime - startTime) / 1_000_000; // 將納秒轉(zhuǎn)換為毫秒 System.out.println("添加一個元素的執(zhí)行時間: " + executionTime + " 毫秒"); // 記錄開始時間 long startTime2 = System.nanoTime(); boolean cacheSet2 = RedisUtils.removeFromCacheSet(key, value2); // 記錄結(jié)束時間 long endTime2 = System.nanoTime(); // 計算執(zhí)行時間(以毫秒為單位) long executionTime2 = (endTime2 - startTime2) / 1_000_000; // 將納秒轉(zhuǎn)換為毫秒 System.out.println("刪除一個元素的代碼執(zhí)行時間: " + executionTime2 + " 毫秒"); }但從時間來講,只有一個字:快
-
查詢占用的內(nèi)存的空間
MEMORY USAGE userId:114514:publication_id:738836
-
? 其實(shí)可以看到,大概是占用66mb,如果用戶的id為雪花算法的id,那可能占用的內(nèi)存100mb
以上來說,主要還是一個bigkey的問題,如果點(diǎn)贊的數(shù)量過大,占用的內(nèi)存過大,寶貴的內(nèi)存不應(yīng)該給這種業(yè)務(wù).
-
自然而然,我們想到用非關(guān)系型數(shù)據(jù)庫,但是不要是基于內(nèi)存的,我想到的是用MongoDB的方案
我們可以往MongoDB中插入一條這樣的數(shù)據(jù):
db.collectionName.insertOne({ "id": "yourIdValue", "userId": yourUserIdValue, "type": yourTypeValue, "likedItemId": yourLikedItemIdValue, "createTime": new Date("yourCreateTimeValue") });id 主鍵id,userId為用戶的ID,type為文章或者動態(tài)或者其他的類型,likedItemId為文章或者動態(tài)或者其他的類型的主鍵ID,createTime為點(diǎn)贊時間
在MongoDB中,可以使用
createIndex方法來創(chuàng)建唯一索引。為userId,type和likedItemId字段創(chuàng)建一個唯一索引。db.collectionName.createIndex( { "userId": 1, "type": 1, "likedItemId": 1 }, { unique: true, name: "unique_index_name" } );詳細(xì)解釋:
-
collectionName:集合名稱。 -
unique_index_name:你想要給索引起的名字,可以根據(jù)你的需求替換為其他名稱。
這個命令將在
collectionName集合上創(chuàng)建一個名為unique_index_name的唯一索引,涵蓋了userId、type和likedItemId字段。1表示升序,如果需要降序索引,可以使用-1。unique: true選項確保索引是唯一的。執(zhí)行這個命令后,如果有重復(fù)的組合出現(xiàn)在這三個字段上,MongoDB將會阻止插入并拋出錯誤。
即如果里面有記錄為已經(jīng)點(diǎn)過贊,點(diǎn)贊就是往里面加記錄,取消點(diǎn)贊就是刪除記錄
詳細(xì)代碼如下:
@Service public class LikeServiceImpl implements LikeService { @Autowired private MongoTemplate mongoTemplate; @Autowired private PublicationService publicationService; /** * 為動態(tài)或者文章點(diǎn)贊 * * @param publicationId 動態(tài)或者文章的ID * @param userId 用戶的ID * @param type 類型,區(qū)分是文章還是動態(tài) * @return 點(diǎn)贊總數(shù) */ @Override public Integer likePublication(Long publicationId, Long userId, Integer type) { // 構(gòu)建查詢條件 Criteria criteria = Criteria.where("userId").is(userId) .and("type").is(type) .and("likedItemId").is(publicationId); // 創(chuàng)建查詢對象并應(yīng)用查詢條件 Query query = new Query(criteria); boolean isExists = mongoTemplate.exists(query, PublicationLike.class); if (isExists) { Asserts.fail("重復(fù)點(diǎn)贊"); } //將點(diǎn)贊記錄保存到mongodb PublicationLike publicationLike = new PublicationLike(); publicationLike.setType(type); publicationLike.setCreateTime(DateUtil.date()); publicationLike.setLikedItemId(publicationId); publicationLike.setUserId(userId); PublicationLike savedLike = mongoTemplate.save(publicationLike); //點(diǎn)贊數(shù)統(tǒng)計 String redisLikeCountKey = String.format(RedisConstant.PUBLICATION_LIKE_COUNT, publicationId, userId, type); Long likeCount = RedisUtils.getAtomicValueWithDefault(redisLikeCountKey); //如果沒有緩存過點(diǎn)贊數(shù),則查詢數(shù)據(jù)庫 if (likeCount.equals(-1L)) { Publication publication = publicationService.getById(publicationId); RedisUtils.setAtomicValue(redisLikeCountKey, publication.getLikeCount()); return publication.getLikeCount(); } else { //返回點(diǎn)贊數(shù)+1 return Math.toIntExact(RedisUtils.incrAtomicValue(redisLikeCountKey)); } } @Override public Integer unlikePublication(Long publicationId, Long userId, Integer type) { // 構(gòu)建查詢條件 Criteria criteria = Criteria.where("userId").is(userId) .and("type").is(type) .and("likedItemId").is(publicationId); // 創(chuàng)建查詢對象并應(yīng)用查詢條件 Query query = new Query(criteria); boolean isExists = mongoTemplate.exists(query, PublicationLike.class); if (!isExists) { Asserts.fail("未點(diǎn)贊過該內(nèi)容,無法取消點(diǎn)贊"); } // 從MongoDB中刪除點(diǎn)贊記錄 mongoTemplate.remove(query, PublicationLike.class); // 更新點(diǎn)贊數(shù)統(tǒng)計 String redisLikeCountKey = String.format(RedisConstant.PUBLICATION_LIKE_COUNT, publicationId, userId, type); Long likeCount = RedisUtils.getAtomicValueWithDefault(redisLikeCountKey); // 如果點(diǎn)贊數(shù)存在于緩存中,減少點(diǎn)贊數(shù)并返回 if (!likeCount.equals(-1L)) { long newLikeCount = RedisUtils.decrAtomicValue(redisLikeCountKey); return Math.toIntExact(newLikeCount); } else { // 如果點(diǎn)贊數(shù)沒有緩存,查詢數(shù)據(jù)庫并更新緩存 Publication publication = publicationService.getById(publicationId); if (publication != null) { RedisUtils.setAtomicValue(redisLikeCountKey, publication.getLikeCount()); return publication.getLikeCount(); } else { Asserts.fail("無法獲取點(diǎn)贊數(shù)"); return 0; } } } } -
總結(jié)
以上是生活随笔為你收集整理的关于点赞业务对MySQL和Redis和MongoDB的思考的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java开发者的Python快速进修指南
- 下一篇: MySQL运行在docker容器中会损失