Redis如何使用zset處理排行榜和計數(shù)問題
Redis使用zset處理排行榜和計數(shù)
在處理計數(shù)業(yè)務(wù)時,我們一般會使用一個數(shù)據(jù)結(jié)構(gòu),既是集合又可以保證唯一性,所以我們會選擇Redis中的set集合:
業(yè)務(wù)邏輯
用戶點擊點贊按鈕,需要再set集合內(nèi)判斷是否已點贊,未點贊則需要將點贊數(shù)+1并保存用戶信息到集合中,已點贊則需要將數(shù)據(jù)庫點贊數(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)點贊
String key = "blog:like:" + id;
Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
if(BooleanUtil.isFalse(isMember)){
// 未點贊
// 數(shù)據(jù)庫點贊數(shù)+1
boolean isSuccess = update().setSql("like = like + 1").eq("id",id).update();
// 保存用戶到Redis集合中
if(isSuccess){
stringRedisTemplate.opsForSet().add(key, userId.toString());
}
} else {
// 已點贊,取消點贊
// 數(shù)據(jù)庫點贊數(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)按照點贊時間的先后順序排序,返回Top5的用戶,這個時候set無法保證數(shù)據(jù)有序,所以我們需要換一個數(shù)據(jù)結(jié)構(gòu)滿足業(yè)務(wù)需求:

Redis 的 ZSET(有序集合) 是一個非常適合用于處理 排行榜 和 計數(shù)問題 的數(shù)據(jù)結(jié)構(gòu)。
在高并發(fā)的點贊業(yè)務(wù)中,使用 ZSET 可以幫助我們高效地管理點贊的排名,并且由于 ZSET 的排序特性,我們可以輕松實現(xiàn)根據(jù)點贊數(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ā)的點贊業(yè)務(wù)中,ZSET 可以幫助我們輕松地進(jìn)行以下幾項操作:
- 記錄每個用戶對某個內(nèi)容(如文章、評論等)的點贊數(shù)。
- 通過分?jǐn)?shù)進(jìn)行實時排序,獲取點贊數(shù)最多的內(nèi)容。
優(yōu)化高并發(fā)的點贊操作
在高并發(fā)情況下,當(dāng)多個用戶同時對某個內(nèi)容進(jìn)行點贊時,我們需要高效地更新該內(nèi)容的點贊數(shù),并保證數(shù)據(jù)一致性。ZSET 提供了很好的支持,具體步驟如下:
- 用戶點贊操作:使用
ZINCRBY命令來對某個元素的分?jǐn)?shù)進(jìn)行增量操作,表示對該內(nèi)容的點贊數(shù)增加。 - 查看點贊數(shù):可以通過
ZSCORE命令獲取某個內(nèi)容的當(dāng)前點贊數(shù)。 - 查看排行榜:使用
ZRANGE或ZREVRANGE命令來獲取點贊數(shù)排名前 N 的內(nèi)容,按分?jǐn)?shù)進(jìn)行排序。
ZSET 結(jié)構(gòu)設(shè)計
key:表示某個內(nèi)容的點贊的 id。value:表示點贊用戶的 id。score:根據(jù)點贊時間排序。
下面是修改后的點贊邏輯:
@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)點贊
String key = "blog:like:" + id;
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
if(score == null){
// 未點贊
// 數(shù)據(jù)庫點贊數(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 {
// 已點贊,取消點贊
// 數(shù)據(jù)庫點贊數(shù)-1
boolean isSuccess = update().setSql("like = like - 1").eq("id",id).update();
// 移除set集合中的用戶
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
return Result.ok();
}
}而點贊排行榜代碼如下:
@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的點贊用戶 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 會按照點贊數(shù)量進(jìn)行排序。
如果直接使用 listByIds 方法,可能會導(dǎo)致結(jié)果順序不一致。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis的主從復(fù)制機(jī)制以及主從復(fù)制的實現(xiàn)方法
本文介紹了Redis的主從復(fù)制機(jī)制,包括異步同步、快照同步、增量同步以及如何配置主從服務(wù)器,主從同步包括增量同步和快照同步兩種方式,增量同步通過指令流實現(xiàn),快照同步通過快照文件傳輸數(shù)據(jù),主從復(fù)制可以提高系統(tǒng)的穩(wěn)定性和性能,但也需要合理配置資源以避免資源浪費2026-02-02
redis key過期監(jiān)聽的實現(xiàn)示例
在Redis中,我們可以為Key設(shè)置過期時間,當(dāng)Key的過期時間到達(dá)后,Redis會自動將該Key標(biāo)記為已失效,本文就來介紹一下redis key過期監(jiān)聽的實現(xiàn)示例,感興趣的可以了解一下2024-03-03
redis使用不當(dāng)導(dǎo)致應(yīng)用卡死bug的過程解析
本文主要記一次找因redis使用不當(dāng)導(dǎo)致應(yīng)用卡死bug的過程,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-07-07

