Redis實(shí)戰(zhàn)(8)-SortedSet典型應(yīng)用場(chǎng)景實(shí)戰(zhàn)之游戲充值排行榜
作者:
修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。
摘要:緩存中間件Redis的數(shù)據(jù)結(jié)構(gòu)~有序集合SortedSet在實(shí)際項(xiàng)目開(kāi)發(fā)中還是比較常見(jiàn)的,特別是在一些諸如“排行榜”的業(yè)務(wù)場(chǎng)景更是經(jīng)常可以見(jiàn)到其身影!本文我們將以項(xiàng)目中實(shí)際的業(yè)務(wù)場(chǎng)景“游戲充值排行榜”為案例,一起來(lái)踐行有序集合SortedSet的“有序 + 唯一”的特性,感受感受其在實(shí)際項(xiàng)目中是如何得到應(yīng)用的!
內(nèi)容:“排行榜”,通俗地講,就是一份榜單,我們小時(shí)候每次考試之后學(xué)校貼出來(lái)的成績(jī)榜其實(shí)就是“排行榜”的一種。顧名思義就是將某些對(duì)象/實(shí)體,比如“某個(gè)人”、“某個(gè)手機(jī)號(hào)”按照某個(gè)值“從大排到小”、“從高排到低”或者“從小到排到大”、“從低排到高”而出來(lái)的一種結(jié)果。
站在程序的角度上看,“排行榜”亦可以說(shuō)是某種“排序算法”運(yùn)行出來(lái)的結(jié)果,典型、常見(jiàn)的業(yè)務(wù)場(chǎng)景包括:手機(jī)充值排行榜、商城積分排行榜、游戲充值排行榜等等…其最終的效果如下圖所示:
由于“排行榜”涉及到“排名”,故而在“放榜”的那一刻,會(huì)有很多小伙伴一擁而上前往觀看,這就類(lèi)似于在某一瞬間,許許多多、并發(fā)產(chǎn)生的線程 請(qǐng)求 查看“排行榜”,而排行榜的數(shù)據(jù)一般是存儲(chǔ)在DB數(shù)據(jù)庫(kù)中的,如果每個(gè)請(qǐng)求過(guò)來(lái)時(shí)都走一遍數(shù)據(jù)庫(kù)查詢(xún)、排序,那無(wú)疑是需要付出很大的代價(jià)的,比如最為明顯的就是某一瞬間DB負(fù)載會(huì)變高、壓力變大,更夸張的可能會(huì)壓垮DB。
因此,我們將想辦法將那些跟排行榜相關(guān)的業(yè)務(wù)數(shù)據(jù)轉(zhuǎn)移到緩存Cache中,并在緩存中實(shí)現(xiàn)業(yè)務(wù)數(shù)據(jù)的排行,最終將得到的排行榜返回給到每個(gè)發(fā)起請(qǐng)求的用戶!
在這里我們使用的緩存Cache便是Redis,并使用其中的數(shù)據(jù)結(jié)構(gòu):有序集合SortedSet加以實(shí)現(xiàn)!SortedSet這種數(shù)據(jù)結(jié)構(gòu)延伸了集合Set的“元素唯一/不重復(fù)”的特性,卻額外增添了不同于集合Set的另外一個(gè)特性:“有序性”,正是這個(gè)“有序性”,才使得我們的“排行榜”業(yè)務(wù)可以得到很好的實(shí)現(xiàn)!
值得一提的是,有序集合SortedSet “有序性”的實(shí)現(xiàn)是通過(guò) “在添加成員時(shí)附帶一個(gè)double類(lèi)型的參數(shù):分?jǐn)?shù)”實(shí)現(xiàn)的,在接下來(lái)的代碼實(shí)戰(zhàn)中,各位小伙伴將會(huì)看到這個(gè)“分?jǐn)?shù)”參數(shù)的無(wú)窮魅力!
接下來(lái)我們以“游戲充值排行榜”為案例,一起來(lái)踐行有序集合SortedSet在實(shí)際業(yè)務(wù)場(chǎng)景的應(yīng)用。對(duì)于“游戲充值排行榜”這一業(yè)務(wù)而言,無(wú)非包含兩個(gè)核心模塊,一個(gè)用戶充值模塊,一個(gè)是用戶獲取排行榜模塊!下面我們將重點(diǎn)來(lái)介紹并實(shí)戰(zhàn)這兩大核心功能模塊
一、用戶游戲充值模塊
對(duì)于用戶充值模塊,玩過(guò)游戲的小伙伴估計(jì)都曉得其大概的業(yè)務(wù)流程,其實(shí)無(wú)非就是輸入手機(jī)號(hào)/游戲賬號(hào)以及金額,然后點(diǎn)擊支付即完成充值的整個(gè)過(guò)程,如下圖所示為該模塊的核心業(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ī)號(hào)碼',
`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ù)庫(kù)表的實(shí)體類(lèi)Entity、Mapper操作接口以及對(duì)應(yīng)的用于寫(xiě)動(dòng)態(tài)SQL的Mapper.xml,在這里就不貼出來(lái)了,各位小伙伴可以前往文末提供的源碼地址進(jìn)行下載觀看!
(2)緊接著我們需要開(kāi)發(fā)一個(gè)SortedSetController,用于前端用戶發(fā)起“充值”的請(qǐng)求,其完整的源代碼如下所示:
/**@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í)體類(lèi)PhoneFare的代碼如下所示:
@Data
@EqualsAndHashCode
public class PhoneFare implements Serializable {
private Integer id;
@NotBlank(message = "手機(jī)號(hào)碼不能為空!")
private String phone;
@NotNull(message = "充值金額不能為空!")
private BigDecimal fare;
private Byte isActive = 1;
}
(3)而sortedSetService.addRecordV2(fare) 要做的事情就是“如何將前端用戶提交過(guò)來(lái)的手機(jī)號(hào)和對(duì)應(yīng)的金額塞到數(shù)據(jù)庫(kù)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ī)號(hào)對(duì)應(yīng)的用戶充過(guò)值了,需要進(jìn)行疊加
zSetOperations.incrementScore(Constant.RedisSortedSetKey2,dto,fare.getFare().doubleValue());
}else{
//TODO:表示只充過(guò)一次話費(fèi)
zSetOperations.add(Constant.RedisSortedSetKey2,dto,fare.getFare().doubleValue());
}
}
return fare.getId();
}
在這里,我們?nèi)氲骄彺鍿ortedSet中的對(duì)象實(shí)體為FareDto類(lèi),該類(lèi)包含一個(gè)字段信息,即“手機(jī)號(hào)”,如下所示:
/**手機(jī)號(hào)唯一性
* @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測(cè)試一波,貼幾張測(cè)試結(jié)果的圖吧:
二、用戶獲取充值排行榜模塊
既然我們的充值都成功插入到了數(shù)據(jù)庫(kù)DB和緩存Cache中,那么接下來(lái)自然而然是需要將其從緩存中獲取出來(lái),并將其處理成“排行榜”的形式展示給用戶觀看,其核心業(yè)務(wù)流程圖如下所示:
(1)同樣的道理, 我們?nèi)匀辉赟ortedSetController中開(kāi)發(fā)“獲取充值排行榜”的請(qǐng)求方法,其完整的源代碼如下所示:
@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測(cè)試一波吧,貼幾張圖:
最終可以看到,展現(xiàn)在我們面前的確實(shí)一張排行榜(從大排到小)!而且這張排行榜是直接從緩存Redis的SortedSet中拿到的,而并非前往數(shù)據(jù)庫(kù)DB進(jìn)行復(fù)雜的查詢(xún)、排序和計(jì)算(無(wú)疑減少了許多數(shù)據(jù)庫(kù)層面的查詢(xún)壓力)!
好了,本篇文章我們就介紹到這里了,建議各位小伙伴一定要照著文章提供的樣例代碼擼一擼,只有擼過(guò)才能知道這玩意是咋用的,否則就成了“空談?wù)摺保?/span>
對(duì)Redis相關(guān)技術(shù)棧以及實(shí)際應(yīng)用場(chǎ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ù)公眾號(hào),或者加Debug的微信,拉你進(jìn)“微信版”的真正技術(shù)交流群!一起學(xué)習(xí)、共同成長(zhǎng)!
補(bǔ)充:
1、本文涉及到的相關(guān)的源代碼可以到此地址,check出來(lái)進(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ù)微信公眾號(hào),最新的技術(shù)文章、課程以及技術(shù)專(zhuān)欄將會(huì)第一時(shí)間在公眾號(hào)發(fā)布哦!