Redis如何使用zset處理排行榜和計數(shù)問題
Redis使用zset處理排行榜和計數(shù)
在處理計數(shù)業(yè)務(wù)時,我們一般會使用一個數(shù)據(jù)結(jié)構(gòu),既是集合又可以保證唯一性,所以我們會選擇Redis中的set集合:
業(yè)務(wù)邏輯
用戶點(diǎn)擊點(diǎn)贊按鈕,需要再set集合內(nèi)判斷是否已點(diǎn)贊,未點(diǎn)贊則需要將點(diǎn)贊數(shù)+1并保存用戶信息到集合中,已點(diǎn)贊則需要將數(shù)據(jù)庫點(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ù)庫點(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ù)庫點(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(); } }
那么我們想要實現(xiàn)按照點(diǎn)贊時間的先后順序排序,返回Top5的用戶,這個時候set無法保證數(shù)據(jù)有序,所以我們需要換一個數(shù)據(jù)結(jié)構(gòu)滿足業(yè)務(wù)需求:
Redis 的 ZSET(有序集合) 是一個非常適合用于處理 排行榜 和 計數(shù)問題 的數(shù)據(jù)結(jié)構(gòu)。
在高并發(fā)的點(diǎn)贊業(yè)務(wù)中,使用 ZSET 可以幫助我們高效地管理點(diǎn)贊的排名,并且由于 ZSET 的排序特性,我們可以輕松實現(xiàn)根據(jù)點(diǎn)贊數(shù)實時排序的功能。
ZSET 數(shù)據(jù)結(jié)構(gòu)
Redis 的 ZSET 是一個集合,它的每個元素都會關(guān)聯(lián)一個 分?jǐn)?shù)(score),這個分?jǐn)?shù)決定了元素在集合中的排序。ZSET 保證集合中的元素是按分?jǐn)?shù)排序的,并且可以在 O(log(N)) 的時間復(fù)雜度內(nèi)進(jìn)行添加、刪除和查找操作。
在高并發(fā)的點(diǎn)贊業(yè)務(wù)中,ZSET 可以幫助我們輕松地進(jìn)行以下幾項操作:
- 記錄每個用戶對某個內(nèi)容(如文章、評論等)的點(diǎn)贊數(shù)。
- 通過分?jǐn)?shù)進(jìn)行實時排序,獲取點(diǎn)贊數(shù)最多的內(nèi)容。
優(yōu)化高并發(fā)的點(diǎn)贊操作
在高并發(fā)情況下,當(dāng)多個用戶同時對某個內(nèi)容進(jìn)行點(diǎn)贊時,我們需要高效地更新該內(nèi)容的點(diǎn)贊數(shù),并保證數(shù)據(jù)一致性。ZSET 提供了很好的支持,具體步驟如下:
- 用戶點(diǎn)贊操作:使用
ZINCRBY
命令來對某個元素的分?jǐn)?shù)進(jìn)行增量操作,表示對該內(nèi)容的點(diǎn)贊數(shù)增加。 - 查看點(diǎn)贊數(shù):可以通過
ZSCORE
命令獲取某個內(nèi)容的當(dāng)前點(diǎn)贊數(shù)。 - 查看排行榜:使用
ZRANGE
或ZREVRANGE
命令來獲取點(diǎn)贊數(shù)排名前 N 的內(nèi)容,按分?jǐn)?shù)進(jìn)行排序。
ZSET 結(jié)構(gòu)設(shè)計
key
:表示某個內(nèi)容的點(diǎn)贊的 id。value
:表示點(diǎn)贊用戶的 id。score
:根據(jù)點(diǎn)贊時間排序。
下面是修改后的點(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ù)庫點(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ù)庫點(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查詢用戶,并將類型由User轉(zhuǎn)為UserDTO,隨后轉(zhuǎn)換為List集合 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 + ")")
來查詢用戶信息,并且使用 order by field(id, ...)
語句來保證查詢結(jié)果的順序與 top5
中的用戶順序一致。
這里的 order by field(id, ...)
是關(guān)鍵,它確保了從數(shù)據(jù)庫返回的數(shù)據(jù)順序和 Redis 返回的 top5
用戶順序完全匹配。因為 Redis 中的 ZSet 是有順序的,top5
會按照點(diǎn)贊數(shù)量進(jìn)行排序。
如果直接使用 listByIds
方法,可能會導(dǎo)致結(jié)果順序不一致。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis實現(xiàn)和數(shù)據(jù)庫的數(shù)據(jù)同步
本文介紹了Redis與傳統(tǒng)數(shù)據(jù)庫數(shù)據(jù)同步的幾種常見方法,包括CacheAside、WriteThrough、WriteBehind,以及如何通過分布式事務(wù)、樂觀鎖、數(shù)據(jù)過期策略和消息隊列來解決數(shù)據(jù)一致性問題,每種方法都有其適用場景和優(yōu)缺點(diǎn),需要根據(jù)具體需求進(jìn)行選擇2025-01-01Redis設(shè)置Hash數(shù)據(jù)類型的過期時間
在Redis中,我們可以使用Hash數(shù)據(jù)結(jié)構(gòu)來存儲一組鍵值對,而有時候,我們可能需要設(shè)置這些鍵值對的過期時間,本文主要介紹了Redis設(shè)置Hash數(shù)據(jù)類型的過期時間,具有一定的參考價值,感興趣的可以了解一下2024-01-01Redis Cluster集群收縮主從節(jié)點(diǎn)詳細(xì)教程
集群收縮的源端就是要下線的主節(jié)點(diǎn),目標(biāo)端就是在線的主節(jié)點(diǎn),這篇文章主要介紹了Redis Cluster集群收縮主從節(jié)點(diǎn)詳細(xì)教程,需要的朋友可以參考下2021-11-11Redis數(shù)據(jù)過期策略的實現(xiàn)詳解
最近項目當(dāng)中遇到一個需求場景,需要清空一些存放在Redis的數(shù)據(jù),本文對Redis的過期機(jī)制簡單的講解一下,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-09-09Redis優(yōu)化token校驗主動失效的實現(xiàn)方案
在普通的token頒發(fā)和校驗中 當(dāng)用戶發(fā)現(xiàn)自己賬號和密碼被暴露了時修改了登錄密碼后舊的token仍然可以通過系統(tǒng)校驗直至token到達(dá)失效時間,所以系統(tǒng)需要token主動失效的一種能力,所以本文給大家介紹了Redis優(yōu)化token校驗主動失效的實現(xiàn)方案,需要的朋友可以參考下2024-03-03使用Redis存儲SpringBoot項目中Session的詳細(xì)步驟
在開發(fā)Spring Boot項目時,我們通常會遇到如何高效管理Session的問題,默認(rèn)情況下,Spring Boot會將Session存儲在內(nèi)存中,今天,我們將學(xué)習(xí)如何將Session存儲從內(nèi)存切換到Redis,并驗證配置是否成功,需要的朋友可以參考下2024-06-06