Redis如何使用zset處理排行榜和計(jì)數(shù)問(wèn)題
Redis使用zset處理排行榜和計(jì)數(shù)
在處理計(jì)數(shù)業(yè)務(wù)時(shí),我們一般會(huì)使用一個(gè)數(shù)據(jù)結(jié)構(gòu),既是集合又可以保證唯一性,所以我們會(huì)選擇Redis中的set集合:
業(yè)務(wù)邏輯
用戶點(diǎn)擊點(diǎn)贊按鈕,需要再set集合內(nèi)判斷是否已點(diǎn)贊,未點(diǎn)贊則需要將點(diǎn)贊數(shù)+1并保存用戶信息到集合中,已點(diǎn)贊則需要將數(shù)據(jù)庫(kù)點(diǎn)贊數(shù)-1并移除set集合中的用戶。
@Service public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService { @Autowired private IUserService userService; @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result likeBlog(Long id) { // 獲取登錄用戶 Long userId = UserHolder.getUser().getId(); // 判斷當(dāng)前登錄用戶是否已經(jīng)點(diǎn)贊 String key = "blog:like:" + id; Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString()); if(BooleanUtil.isFalse(isMember)){ // 未點(diǎn)贊 // 數(shù)據(jù)庫(kù)點(diǎn)贊數(shù)+1 boolean isSuccess = update().setSql("like = like + 1").eq("id",id).update(); // 保存用戶到Redis集合中 if(isSuccess){ stringRedisTemplate.opsForSet().add(key, userId.toString()); } } else { // 已點(diǎn)贊,取消點(diǎn)贊 // 數(shù)據(jù)庫(kù)點(diǎn)贊數(shù)-1 boolean isSuccess = update().setSql("like = like - 1").eq("id",id).update(); // 移除set集合中的用戶 stringRedisTemplate.opsForSet().remove(key, userId.toString()); } return Result.ok(); } }
那么我們想要實(shí)現(xiàn)按照點(diǎn)贊時(shí)間的先后順序排序,返回Top5的用戶,這個(gè)時(shí)候set無(wú)法保證數(shù)據(jù)有序,所以我們需要換一個(gè)數(shù)據(jù)結(jié)構(gòu)滿足業(yè)務(wù)需求:
Redis 的 ZSET(有序集合) 是一個(gè)非常適合用于處理 排行榜 和 計(jì)數(shù)問(wèn)題 的數(shù)據(jù)結(jié)構(gòu)。
在高并發(fā)的點(diǎn)贊業(yè)務(wù)中,使用 ZSET 可以幫助我們高效地管理點(diǎn)贊的排名,并且由于 ZSET 的排序特性,我們可以輕松實(shí)現(xiàn)根據(jù)點(diǎn)贊數(shù)實(shí)時(shí)排序的功能。
ZSET 數(shù)據(jù)結(jié)構(gòu)
Redis 的 ZSET 是一個(gè)集合,它的每個(gè)元素都會(huì)關(guān)聯(lián)一個(gè) 分?jǐn)?shù)(score),這個(gè)分?jǐn)?shù)決定了元素在集合中的排序。ZSET 保證集合中的元素是按分?jǐn)?shù)排序的,并且可以在 O(log(N)) 的時(shí)間復(fù)雜度內(nèi)進(jìn)行添加、刪除和查找操作。
在高并發(fā)的點(diǎn)贊業(yè)務(wù)中,ZSET 可以幫助我們輕松地進(jìn)行以下幾項(xiàng)操作:
- 記錄每個(gè)用戶對(duì)某個(gè)內(nèi)容(如文章、評(píng)論等)的點(diǎn)贊數(shù)。
- 通過(guò)分?jǐn)?shù)進(jìn)行實(shí)時(shí)排序,獲取點(diǎn)贊數(shù)最多的內(nèi)容。
優(yōu)化高并發(fā)的點(diǎn)贊操作
在高并發(fā)情況下,當(dāng)多個(gè)用戶同時(shí)對(duì)某個(gè)內(nèi)容進(jìn)行點(diǎn)贊時(shí),我們需要高效地更新該內(nèi)容的點(diǎn)贊數(shù),并保證數(shù)據(jù)一致性。ZSET 提供了很好的支持,具體步驟如下:
- 用戶點(diǎn)贊操作:使用
ZINCRBY
命令來(lái)對(duì)某個(gè)元素的分?jǐn)?shù)進(jìn)行增量操作,表示對(duì)該內(nèi)容的點(diǎn)贊數(shù)增加。 - 查看點(diǎn)贊數(shù):可以通過(guò)
ZSCORE
命令獲取某個(gè)內(nèi)容的當(dāng)前點(diǎn)贊數(shù)。 - 查看排行榜:使用
ZRANGE
或ZREVRANGE
命令來(lái)獲取點(diǎn)贊數(shù)排名前 N 的內(nèi)容,按分?jǐn)?shù)進(jìn)行排序。
ZSET 結(jié)構(gòu)設(shè)計(jì)
key
:表示某個(gè)內(nèi)容的點(diǎn)贊的 id。value
:表示點(diǎn)贊用戶的 id。score
:根據(jù)點(diǎn)贊時(shí)間排序。
下面是修改后的點(diǎn)贊邏輯:
@Service public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService { @Autowired private IUserService userService; @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result likeBlog(Long id) { // 獲取登錄用戶 Long userId = UserHolder.getUser().getId(); // 判斷當(dāng)前登錄用戶是否已經(jīng)點(diǎn)贊 String key = "blog:like:" + id; Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); if(score == null){ // 未點(diǎn)贊 // 數(shù)據(jù)庫(kù)點(diǎn)贊數(shù)+1 boolean isSuccess = update().setSql("like = like + 1").eq("id",id).update(); // 保存用戶到Redis集合中 if(isSuccess){ stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis()); } } else { // 已點(diǎn)贊,取消點(diǎn)贊 // 數(shù)據(jù)庫(kù)點(diǎn)贊數(shù)-1 boolean isSuccess = update().setSql("like = like - 1").eq("id",id).update(); // 移除set集合中的用戶 stringRedisTemplate.opsForZSet().remove(key, userId.toString()); } return Result.ok(); } }
而點(diǎn)贊排行榜代碼如下:
@Service public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService { @Autowired private IUserService userService; @Resource private StringRedisTemplate stringRedisTemplate; @Override public Result queryBlogLikes(Long id) { String key = "blog:like:" + id; // 查詢top5的點(diǎn)贊用戶 zrange key 0 4 Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4); if (top5 == null || top5.isEmpty()) { return Result.ok(Collections.emptyList()); } // 解析出集合中的用戶的id List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList()); // 根據(jù)id查詢用戶,并將類(lèi)型由User轉(zhuǎn)為UserDTO,隨后轉(zhuǎn)換為L(zhǎng)ist集合 String idStr = StrUtil.join(",",ids); // List<UserDTO> userDTOs = userService.listByIds(ids).stream() // .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) // .collect(Collectors.toList()); List<UserDTO> userDTOs = userService.query() .in("id",ids).last("order by field(id," + idStr +")").list() .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); return Result.ok(userDTOs); } }
使用
userService.query().in("id", ids).last("order by field(id," + idStr + ")")
來(lái)查詢用戶信息,并且使用 order by field(id, ...)
語(yǔ)句來(lái)保證查詢結(jié)果的順序與 top5
中的用戶順序一致。
這里的 order by field(id, ...)
是關(guān)鍵,它確保了從數(shù)據(jù)庫(kù)返回的數(shù)據(jù)順序和 Redis 返回的 top5
用戶順序完全匹配。因?yàn)?Redis 中的 ZSet 是有順序的,top5
會(huì)按照點(diǎn)贊數(shù)量進(jìn)行排序。
如果直接使用 listByIds
方法,可能會(huì)導(dǎo)致結(jié)果順序不一致。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis 中的布隆過(guò)濾器的實(shí)現(xiàn)
這篇文章主要介紹了Redis 中的布隆過(guò)濾器的實(shí)現(xiàn),詳細(xì)的介紹了什么是布隆過(guò)濾器以及如何實(shí)現(xiàn),非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-10-10Redis實(shí)現(xiàn)和數(shù)據(jù)庫(kù)的數(shù)據(jù)同步
本文介紹了Redis與傳統(tǒng)數(shù)據(jù)庫(kù)數(shù)據(jù)同步的幾種常見(jiàn)方法,包括CacheAside、WriteThrough、WriteBehind,以及如何通過(guò)分布式事務(wù)、樂(lè)觀鎖、數(shù)據(jù)過(guò)期策略和消息隊(duì)列來(lái)解決數(shù)據(jù)一致性問(wèn)題,每種方法都有其適用場(chǎng)景和優(yōu)缺點(diǎn),需要根據(jù)具體需求進(jìn)行選擇2025-01-01redis通過(guò)位圖法記錄在線用戶的狀態(tài)詳解
這篇文章主要給大家介紹了關(guān)于redis如何通過(guò)位圖法記錄在線用戶的狀態(tài)的相關(guān)資料,文中先對(duì)位圖進(jìn)行了一個(gè)簡(jiǎn)單的介紹,而后通過(guò)示例代碼將實(shí)現(xiàn)的方法介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11Redis設(shè)置Hash數(shù)據(jù)類(lèi)型的過(guò)期時(shí)間
在Redis中,我們可以使用Hash數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)一組鍵值對(duì),而有時(shí)候,我們可能需要設(shè)置這些鍵值對(duì)的過(guò)期時(shí)間,本文主要介紹了Redis設(shè)置Hash數(shù)據(jù)類(lèi)型的過(guò)期時(shí)間,具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01詳解redis緩存與數(shù)據(jù)庫(kù)一致性問(wèn)題解決
這篇文章主要介紹了詳解redis緩存與數(shù)據(jù)庫(kù)一致性問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Redis Cluster集群收縮主從節(jié)點(diǎn)詳細(xì)教程
集群收縮的源端就是要下線的主節(jié)點(diǎn),目標(biāo)端就是在線的主節(jié)點(diǎn),這篇文章主要介紹了Redis Cluster集群收縮主從節(jié)點(diǎn)詳細(xì)教程,需要的朋友可以參考下2021-11-11Redis數(shù)據(jù)過(guò)期策略的實(shí)現(xiàn)詳解
最近項(xiàng)目當(dāng)中遇到一個(gè)需求場(chǎng)景,需要清空一些存放在Redis的數(shù)據(jù),本文對(duì)Redis的過(guò)期機(jī)制簡(jiǎn)單的講解一下,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09Redis優(yōu)化token校驗(yàn)主動(dòng)失效的實(shí)現(xiàn)方案
在普通的token頒發(fā)和校驗(yàn)中 當(dāng)用戶發(fā)現(xiàn)自己賬號(hào)和密碼被暴露了時(shí)修改了登錄密碼后舊的token仍然可以通過(guò)系統(tǒng)校驗(yàn)直至token到達(dá)失效時(shí)間,所以系統(tǒng)需要token主動(dòng)失效的一種能力,所以本文給大家介紹了Redis優(yōu)化token校驗(yàn)主動(dòng)失效的實(shí)現(xiàn)方案,需要的朋友可以參考下2024-03-03使用Redis存儲(chǔ)SpringBoot項(xiàng)目中Session的詳細(xì)步驟
在開(kāi)發(fā)Spring Boot項(xiàng)目時(shí),我們通常會(huì)遇到如何高效管理Session的問(wèn)題,默認(rèn)情況下,Spring Boot會(huì)將Session存儲(chǔ)在內(nèi)存中,今天,我們將學(xué)習(xí)如何將Session存儲(chǔ)從內(nèi)存切換到Redis,并驗(yàn)證配置是否成功,需要的朋友可以參考下2024-06-06