Redis實(shí)戰(zhàn)(8)-SortedSet典型應(yīng)用場景實(shí)戰(zhàn)之游戲充值排行榜

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。



摘要:緩存中間件Redis的數(shù)據(jù)結(jié)構(gòu)~有序集合SortedSet在實(shí)際項(xiàng)目開發(fā)中還是比較常見的,特別是在一些諸如“排行榜”的業(yè)務(wù)場景更是經(jīng)??梢砸姷狡渖碛?!本文我們將以項(xiàng)目中實(shí)際的業(yè)務(wù)場景“游戲充值排行榜”為案例,一起來踐行有序集合SortedSet的“有序 + 唯一”的特性,感受感受其在實(shí)際項(xiàng)目中是如何得到應(yīng)用的!

內(nèi)容:“排行榜”,通俗地講,就是一份榜單,我們小時候每次考試之后學(xué)校貼出來的成績榜其實(shí)就是“排行榜”的一種。顧名思義就是將某些對象/實(shí)體,比如“某個人”、“某個手機(jī)號”按照某個值“從大排到小”、“從高排到低”或者“從小到排到大”、“從低排到高”而出來的一種結(jié)果。

站在程序的角度上看,“排行榜”亦可以說是某種“排序算法”運(yùn)行出來的結(jié)果,典型、常見的業(yè)務(wù)場景包括:手機(jī)充值排行榜、商城積分排行榜、游戲充值排行榜等等…其最終的效果如下圖所示:


由于“排行榜”涉及到“排名”,故而在“放榜”的那一刻,會有很多小伙伴一擁而上前往觀看,這就類似于在某一瞬間,許許多多、并發(fā)產(chǎn)生的線程 請求 查看“排行榜”,而排行榜的數(shù)據(jù)一般是存儲在DB數(shù)據(jù)庫中的,如果每個請求過來時都走一遍數(shù)據(jù)庫查詢、排序,那無疑是需要付出很大的代價的,比如最為明顯的就是某一瞬間DB負(fù)載會變高、壓力變大,更夸張的可能會壓垮DB。

因此,我們將想辦法將那些跟排行榜相關(guān)的業(yè)務(wù)數(shù)據(jù)轉(zhuǎn)移到緩存Cache中,并在緩存中實(shí)現(xiàn)業(yè)務(wù)數(shù)據(jù)的排行,最終將得到的排行榜返回給到每個發(fā)起請求的用戶!

在這里我們使用的緩存Cache便是Redis,并使用其中的數(shù)據(jù)結(jié)構(gòu):有序集合SortedSet加以實(shí)現(xiàn)!SortedSet這種數(shù)據(jù)結(jié)構(gòu)延伸了集合Set的“元素唯一/不重復(fù)”的特性,卻額外增添了不同于集合Set的另外一個特性:“有序性”,正是這個“有序性”,才使得我們的“排行榜”業(yè)務(wù)可以得到很好的實(shí)現(xiàn)!

值得一提的是,有序集合SortedSet “有序性”的實(shí)現(xiàn)是通過 “在添加成員時附帶一個double類型的參數(shù):分?jǐn)?shù)”實(shí)現(xiàn)的,在接下來的代碼實(shí)戰(zhàn)中,各位小伙伴將會看到這個“分?jǐn)?shù)”參數(shù)的無窮魅力!

接下來我們以“游戲充值排行榜”為案例,一起來踐行有序集合SortedSet在實(shí)際業(yè)務(wù)場景的應(yīng)用。對于“游戲充值排行榜”這一業(yè)務(wù)而言,無非包含兩個核心模塊,一個用戶充值模塊,一個是用戶獲取排行榜模塊!下面我們將重點(diǎn)來介紹并實(shí)戰(zhàn)這兩大核心功能模塊

一、用戶游戲充值模塊

對于用戶充值模塊,玩過游戲的小伙伴估計都曉得其大概的業(yè)務(wù)流程,其實(shí)無非就是輸入手機(jī)號/游戲賬號以及金額,然后點(diǎn)擊支付即完成充值的整個過程,如下圖所示為該模塊的核心業(yè)務(wù)流程圖:


下面,我們進(jìn)入代碼實(shí)戰(zhàn)環(huán)節(jié)!

(1)同樣的道理,工欲善其事,必先利其器,我們先建立一張用于記錄 用戶歷史充值記錄的“用戶充值表”,其DDL如下所示:

CREATE TABLE `phone_fare` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phone` varchar(50) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '手機(jī)號碼',
`fare` decimal(10,2) DEFAULT NULL COMMENT '充值金額',
`is_active` tinyint(4) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
PRIMARY KEY (`id`),
KEY `idx_phone` (`phone`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='手機(jī)充值記錄';

采用Mybatis逆向工程或者代碼生成器生成該數(shù)據(jù)庫表的實(shí)體類Entity、Mapper操作接口以及對應(yīng)的用于寫動態(tài)SQL的Mapper.xml,在這里就不貼出來了,各位小伙伴可以前往文末提供的源碼地址進(jìn)行下載觀看!

(2)緊接著我們需要開發(fā)一個SortedSetController,用于前端用戶發(fā)起“充值”的請求,其完整的源代碼如下所示:

/**@Author:debug (SteadyJack)  weixin-> debug0868 qq-> 1948831260
**/
@RestController
@RequestMapping("sorted/set")
public class SortedSetController extends AbstractController {

@Autowired
private SortedSetService sortedSetService;

@RequestMapping(value = "put/v2",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public BaseResponse putv2(@RequestBody @Validated PhoneFare fare, BindingResult result){
String checkRes= ValidatorUtil.checkResult(result);
if (StrUtil.isNotBlank(checkRes)){
return new BaseResponse(StatusCode.Fail.getCode(),checkRes);
}
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
response.setData(sortedSetService.addRecordV2(fare));
}catch (Exception e){
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
}

其中,實(shí)體類PhoneFare的代碼如下所示:  

@Data
@EqualsAndHashCode
public class PhoneFare implements Serializable {
private Integer id;

@NotBlank(message = "手機(jī)號碼不能為空!")
private String phone;

@NotNull(message = "充值金額不能為空!")
private BigDecimal fare;

private Byte isActive = 1;
}

(3)而sortedSetService.addRecordV2(fare) 要做的事情就是“如何將前端用戶提交過來的手機(jī)號和對應(yīng)的金額塞到數(shù)據(jù)庫DB和緩存Redis中去”,其完整的源代碼如下所示:  

    //TODO:新增/手機(jī)話費(fèi)充值 記錄 v2
@Transactional(rollbackFor = Exception.class)
public Integer addRecordV2(PhoneFare fare) throws Exception{
log.info("----sorted set話費(fèi)充值記錄新增V2:{} ",fare);

int res=fareMapper.insertSelective(fare);
if (res>0){
FareDto dto=new FareDto(fare.getPhone());

ZSetOperations<String,FareDto> zSetOperations=redisTemplate.opsForZSet();
Double oldFare=zSetOperations.score(Constant.RedisSortedSetKey2,dto);
if (oldFare!=null){
//TODO:表示之前該手機(jī)號對應(yīng)的用戶充過值了,需要進(jìn)行疊加
zSetOperations.incrementScore(Constant.RedisSortedSetKey2,dto,fare.getFare().doubleValue());
}else{
//TODO:表示只充過一次話費(fèi)
zSetOperations.add(Constant.RedisSortedSetKey2,dto,fare.getFare().doubleValue());
}
}
return fare.getId();
}

在這里,我們?nèi)氲骄彺鍿ortedSet中的對象實(shí)體為FareDto類,該類包含一個字段信息,即“手機(jī)號”,如下所示:  

/**手機(jī)號唯一性
* @Author:debug (SteadyJack) weixin-> debug0868 qq-> 1948831260 **/
@Data
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class FareDto implements Serializable{
private String phone;
}

(4)至此,我們已經(jīng)完成了“用戶充值”業(yè)務(wù)模塊的功能,下面我們用Postman測試一波,貼幾張測試結(jié)果的圖吧:



二、用戶獲取充值排行榜模塊

既然我們的充值都成功插入到了數(shù)據(jù)庫DB和緩存Cache中,那么接下來自然而然是需要將其從緩存中獲取出來,并將其處理成“排行榜”的形式展示給用戶觀看,其核心業(yè)務(wù)流程圖如下所示:


(1)同樣的道理, 我們?nèi)匀辉赟ortedSetController中開發(fā)“獲取充值排行榜”的請求方法,其完整的源代碼如下所示:

    @RequestMapping(value = "get/v2",method = RequestMethod.GET)
public BaseResponse getV2(){
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
response.setData(sortedSetService.getSortFaresV2());
}catch (Exception e) {
response = new BaseResponse(StatusCode.Fail.getCode(), e.getMessage());
}
return response;
}

(2)而 sortedSetService.getSortFaresV2() 做的事情便是實(shí)現(xiàn)如何從緩存Redis的有序集合“SortedSet中獲取到充值排行榜”,其完整源碼如下所示:  

    //TODO:獲取充值排行榜V2
public List<PhoneFare> getSortFaresV2(){
List<PhoneFare> list= Lists.newLinkedList();

final String key=Constant.RedisSortedSetKey2;
ZSetOperations<String,FareDto> zSetOperations=redisTemplate.opsForZSet();
final Long size=zSetOperations.size(key);

Set<ZSetOperations.TypedTuple<FareDto>> set=zSetOperations.reverseRangeWithScores(key,0L,size);
if (set!=null && !set.isEmpty()){
set.forEach(tuple -> {
PhoneFare fare=new PhoneFare();
fare.setFare(BigDecimal.valueOf(tuple.getScore()));
fare.setPhone(tuple.getValue().getPhone());

list.add(fare);
});
}
return list;
}

(3)至此,我們已經(jīng)將“獲取用戶充值排行榜”的功能模塊實(shí)戰(zhàn)完畢,下面我們也同樣基于Postman測試一波吧,貼幾張圖:


最終可以看到,展現(xiàn)在我們面前的確實(shí)一張排行榜(從大排到?。?!而且這張排行榜是直接從緩存Redis的SortedSet中拿到的,而并非前往數(shù)據(jù)庫DB進(jìn)行復(fù)雜的查詢、排序和計算(無疑減少了許多數(shù)據(jù)庫層面的查詢壓力)!

好了,本篇文章我們就介紹到這里了,建議各位小伙伴一定要照著文章提供的樣例代碼擼一擼,只有擼過才能知道這玩意是咋用的,否則就成了“空談?wù)摺保?/span>

對Redis相關(guān)技術(shù)棧以及實(shí)際應(yīng)用場景實(shí)戰(zhàn)感興趣的小伙伴可以前往Debug搭建的技術(shù)社區(qū)的課程中心進(jìn)行學(xué)習(xí)觀看:https://www.fightjava.com/web/index/course/detail/12 !

其他相關(guān)的技術(shù),感興趣的小伙伴可以關(guān)注底部Debug的技術(shù)公眾號,或者加Debug的微信,拉你進(jìn)“微信版”的真正技術(shù)交流群!一起學(xué)習(xí)、共同成長!

補(bǔ)充:

1、本文涉及到的相關(guān)的源代碼可以到此地址,check出來進(jìn)行查看學(xué)習(xí):

https://gitee.com/steadyjack/SpringBootRedis

2、目前Debug已將本文所涉及的內(nèi)容整理錄制成視頻教程,感興趣的小伙伴可以前往觀看學(xué)習(xí):https://www.fightjava.com/web/index/course/detail/12

3、關(guān)注一下Debug的技術(shù)微信公眾號,最新的技術(shù)文章、課程以及技術(shù)專欄將會第一時間在公眾號發(fā)布哦!