中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁(yè) > news >正文

網(wǎng)站案例欣賞seo機(jī)構(gòu)

網(wǎng)站案例欣賞,seo機(jī)構(gòu),石家莊網(wǎng)站建設(shè)推廣公司報(bào)價(jià),濟(jì)南網(wǎng)站排名推廣🎯 本文檔介紹了場(chǎng)館預(yù)訂系統(tǒng)接口V2的設(shè)計(jì)與實(shí)現(xiàn),旨在解決V1版本中庫(kù)存數(shù)據(jù)不一致及性能瓶頸的問題。通過(guò)引入令牌機(jī)制確保緩存和數(shù)據(jù)庫(kù)庫(kù)存的最終一致性,避免因服務(wù)器故障導(dǎo)致的庫(kù)存錯(cuò)誤占用問題。同時(shí),采用消息隊(duì)列異步處理庫(kù)存…

🎯 本文檔介紹了場(chǎng)館預(yù)訂系統(tǒng)接口V2的設(shè)計(jì)與實(shí)現(xiàn),旨在解決V1版本中庫(kù)存數(shù)據(jù)不一致及性能瓶頸的問題。通過(guò)引入令牌機(jī)制確保緩存和數(shù)據(jù)庫(kù)庫(kù)存的最終一致性,避免因服務(wù)器故障導(dǎo)致的庫(kù)存錯(cuò)誤占用問題。同時(shí),采用消息隊(duì)列異步處理庫(kù)存扣減和訂單創(chuàng)建,顯著提升了接口的吞吐量和響應(yīng)速度。測(cè)試結(jié)果顯示,新版接口在高并發(fā)場(chǎng)景下表現(xiàn)優(yōu)異,平均響應(yīng)時(shí)間為1801毫秒,吞吐量達(dá)到了每秒1045.8次請(qǐng)求,異常率僅為0.22%,極大改善了用戶體驗(yàn)。
🏠? HelloDam/場(chǎng)快訂(場(chǎng)館預(yù)定 SaaS 平臺(tái))

文章目錄

  • 說(shuō)明
  • 避免空?qǐng)鰺o(wú)法預(yù)訂
  • 接口性能提升
    • Controller
    • Service
    • MQ
      • 生產(chǎn)者
      • 消費(fèi)者
  • 測(cè)試結(jié)果
  • 說(shuō)明

說(shuō)明

在閱讀此文之前,建議先閱讀預(yù)訂接口V1實(shí)現(xiàn):https://hellodam.blog.csdn.net/article/details/144950335

接口 V2 主要是解決 V1 存在的一些問題:

  • 問題一:接口 V1 中存在如下問題:假如說(shuō) lua 腳本執(zhí)行完成,緩存中的庫(kù)存已經(jīng)扣減,結(jié)果突然服務(wù)器宕機(jī)了,沒有執(zhí)行后續(xù)的數(shù)據(jù)庫(kù)庫(kù)存扣減和創(chuàng)建訂單流程,就會(huì)出現(xiàn)庫(kù)存被錯(cuò)誤占用,導(dǎo)致緩存中庫(kù)存小于實(shí)際庫(kù)存。對(duì)應(yīng)于現(xiàn)實(shí),就是有的場(chǎng)空著,用戶預(yù)定不到
  • 問題二:接口 V1 中,因?yàn)閹?kù)存扣減和訂單創(chuàng)建是同步的,預(yù)訂接口吞吐量較低。為了進(jìn)一步提升接口性能,可以使用消息隊(duì)列來(lái)異步執(zhí)行庫(kù)存扣減和訂單創(chuàng)建邏輯

避免空?qǐng)鰺o(wú)法預(yù)訂

緩存扣減完成之后,由于發(fā)生故障,導(dǎo)致沒有更新數(shù)據(jù)庫(kù)。這個(gè)問題本身是無(wú)法避免的,只能通過(guò)一些機(jī)制來(lái)兜底。本文通過(guò)使用令牌機(jī)制來(lái)解決空?qǐng)鰺o(wú)法預(yù)訂問題。

在接口 V1 中,用戶請(qǐng)求預(yù)定接口,先查看 Redis 緩存中的庫(kù)存是否大于 0 ,大于 0 才進(jìn)行后面的操作。令牌是什么,其實(shí)也是這個(gè)緩存,但是我們并不完全相信它,我們知道它可能和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致。當(dāng)用戶獲取不到令牌的時(shí)候,我們不是直接返回時(shí)間段售罄錯(cuò)誤,而是先查詢一下數(shù)據(jù)庫(kù),看看是不是真的售罄了,如果數(shù)據(jù)庫(kù)中還有庫(kù)存,就刪除令牌緩存。這樣下一個(gè)用戶再發(fā)起預(yù)訂時(shí),就會(huì)重新刷新令牌緩存,這樣令牌的數(shù)據(jù)就和數(shù)據(jù)庫(kù)保持一致,就不會(huì)出現(xiàn)空?qǐng)鰺o(wú)法預(yù)訂的問題。

為了實(shí)現(xiàn)這個(gè)思路,我們還需要考慮一個(gè)問題,難道每個(gè)用戶看到?jīng)]有令牌都去查數(shù)據(jù)庫(kù)嗎,那肯定不行,這樣并發(fā)高的話,數(shù)據(jù)庫(kù)很容易被打崩??梢酝ㄟ^(guò)分布式鎖讓同一時(shí)刻只有一個(gè)用戶查詢數(shù)據(jù)庫(kù),但是光是添加分布式鎖還是不行,用戶請(qǐng)求多時(shí),可能出現(xiàn)不同時(shí)間點(diǎn)連續(xù)查詢數(shù)據(jù)庫(kù)刷新token的情況,其實(shí)不必如此頻繁查詢。還有一個(gè)問題,高并發(fā)時(shí)大量任務(wù)等著數(shù)據(jù)庫(kù)響應(yīng),數(shù)據(jù)庫(kù)更新不會(huì)那么快。如果是立刻刷新token,可能出現(xiàn)數(shù)據(jù)庫(kù)沒來(lái)得及扣減庫(kù)存,就被刷新到token中了,這樣會(huì)導(dǎo)致時(shí)間段超賣,因?yàn)榱钆茢?shù)量大于庫(kù)存。為了解決上述問題,可以先延時(shí)10秒再刷新token,在這10秒內(nèi),其他用戶訪問預(yù)定接口,因?yàn)槟貌坏椒植际芥i,也不會(huì)重復(fù)執(zhí)行token刷新。

/*** 查詢數(shù)據(jù)庫(kù)是否還有庫(kù)存,如果還有的話,刪除令牌,讓下一個(gè)用戶重新加載令牌緩存** @param timePeriodId*/
private void refreshTokenByCheckDatabase(Long timePeriodId) {RLock lock = redissonClient.getLock(String.format(RedisCacheConstant.VENUE_LOCK_TIME_PERIOD_REFRESH_TOKEN_KEY, timePeriodId));// 嘗試獲取分布式鎖,獲取不成功直接返回if (!lock.tryLock()) {return;}// 延遲 10 秒之后去檢查數(shù)據(jù)庫(kù)和令牌是否一致// 為啥要延遲?如果不延遲的話,可能高峰期時(shí),大量請(qǐng)求過(guò)來(lái),數(shù)據(jù)庫(kù)還沒來(lái)得及更新,就觸發(fā)令牌刷新,導(dǎo)致超賣tokenRefreshExecutor.schedule(() -> {try {TimePeriodDO timePeriodDO = this.getById(timePeriodId);if (timePeriodDO.getStock() > 0) {// --if-- 數(shù)據(jù)庫(kù)中還有庫(kù)存,說(shuō)明數(shù)據(jù)庫(kù)中的庫(kù)存和令牌中的庫(kù)存不一致,刪除緩存,讓下一個(gè)用戶重新獲取stringRedisTemplate.delete(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY);stringRedisTemplate.delete(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY);}} finally {lock.unlock();}}, 10, TimeUnit.SECONDS);
}

接口性能提升

Controller

/*** 預(yù)定時(shí)間段*/
@GetMapping("/v2/reserve")
@Idempotent(uniqueKeyPrefix = "vrs-venue:lock_reserve:",// 讓用戶同時(shí)最多只能預(yù)定一個(gè)時(shí)間段,根據(jù)用戶名來(lái)加鎖// key = "T(com.vrs.common.context.UserContext).getUsername()",// 讓用戶同時(shí)最多只能預(yù)定該時(shí)間段一次,但是可以同時(shí)預(yù)定其他時(shí)間段,根據(jù)用戶名+時(shí)間段ID來(lái)加鎖key = "T(com.vrs.common.context.UserContext).getUsername()+'_'+#timePeriodId",message = "正在執(zhí)行場(chǎng)館預(yù)定流程,請(qǐng)勿重復(fù)預(yù)定...",scene = IdempotentSceneEnum.RESTAPI
)
@Operation(summary = "預(yù)定時(shí)間段V2")
public Result reserve2(@RequestParam("timePeriodId") Long timePeriodId) {timePeriodService.reserve2(timePeriodId);return Results.success();
}

Service

【預(yù)訂流程】

  • 參數(shù)檢驗(yàn)
  • 獲取令牌
    • 能獲取到,執(zhí)行下一步
    • 獲取不到,查詢數(shù)據(jù)庫(kù),刷新令牌緩存
  • 發(fā)送消息,異步更新庫(kù)存并生成訂單
/*** 嘗試獲取令牌,令牌獲取成功之后,發(fā)送消息,異步執(zhí)行庫(kù)存扣減和訂單生成* 注意:令牌在極端情況下,如扣減令牌之后,服務(wù)宕機(jī)了,此時(shí)令牌的庫(kù)存是小于真實(shí)庫(kù)存的* 如果查詢令牌發(fā)現(xiàn)庫(kù)存為0,嘗試去數(shù)據(jù)庫(kù)中加載數(shù)據(jù),加載之后庫(kù)存還是0,說(shuō)明時(shí)間段確實(shí)售罄了* 使用消息隊(duì)列異步 扣減庫(kù)存,更新緩存,生成訂單** @param timePeriodId*/
@Override
public void reserve2(Long timePeriodId) { 參數(shù)校驗(yàn):使用責(zé)任鏈模式校驗(yàn)數(shù)據(jù)是否正確TimePeriodReserveReqDTO timePeriodReserveReqDTO = new TimePeriodReserveReqDTO(timePeriodId);chainContext.handler(ChainConstant.RESERVE_CHAIN_NAME, timePeriodReserveReqDTO);TimePeriodDO timePeriodDO = timePeriodReserveReqDTO.getTimePeriodDO();Long venueId = timePeriodReserveReqDTO.getVenueId();VenueDO venueDO = timePeriodReserveReqDTO.getVenueDO();PartitionDO partitionDO = partitionService.getPartitionDOById(timePeriodDO.getPartitionId()); 使用lua腳本獲取一個(gè)空?qǐng)龅貙?duì)應(yīng)的索引,并扣除相應(yīng)的庫(kù)存,同時(shí)在里面進(jìn)行用戶的查重// 首先檢測(cè)空閑場(chǎng)號(hào)緩存有沒有加載好,沒有的話進(jìn)行加載this.checkBitMapCache(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY, timePeriodReserveReqDTO.getTimePeriodId()),timePeriodId,partitionDO.getNum());// 其次檢測(cè)時(shí)間段庫(kù)存有沒有加載好,沒有的話進(jìn)行加載this.getStockByTimePeriodId(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY, timePeriodReserveReqDTO.getTimePeriodId());// 執(zhí)行l(wèi)ua腳本Long freeCourtIndex = executeStockReduceByLua(timePeriodReserveReqDTO,venueDO,RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY,RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY);if (freeCourtIndex == -2L) {// --if-- 用戶已經(jīng)購(gòu)買過(guò)該時(shí)間段throw new ClientException(BaseErrorCode.TIME_PERIOD_HAVE_BOUGHT_ERROR);} else if (freeCourtIndex == -1L) {// --if-- 沒有空閑的場(chǎng)號(hào),查詢數(shù)據(jù)庫(kù),如果數(shù)據(jù)庫(kù)中有庫(kù)存,刪除緩存,下一個(gè)用戶預(yù)定時(shí)重新加載令牌this.refreshTokenByCheckDatabase(timePeriodId);throw new ServiceException(BaseErrorCode.TIME_PERIOD_SELL_OUT_ERROR);} 發(fā)送消息,異步更新庫(kù)存并生成訂單SendResult sendResult = executeReserveProducer.sendMessage(ExecuteReserveMqDTO.builder().timePeriodId(timePeriodId).freeCourtIndex(freeCourtIndex).venueId(venueId).userId(UserContext.getUserId()).userName(UserContext.getUsername()).build());if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)) {log.error("消息發(fā)送失敗: " + sendResult.getSendStatus());// 恢復(fù)令牌緩存this.restoreStockAndBookedSlotsCache(timePeriodId,UserContext.getUserId(),freeCourtIndex,RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY,RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY);throw new ServiceException(BaseErrorCode.MQ_SEND_ERROR);}
}

【獲取令牌】

獲取令牌的過(guò)程其實(shí)就是 檢驗(yàn)用戶是否重新預(yù)訂、庫(kù)存數(shù)量檢查、場(chǎng)號(hào)分配、庫(kù)存扣減、場(chǎng)號(hào)占用 ,這里和接口V1的實(shí)現(xiàn)是一樣的

/*** 使用lua腳本,進(jìn)行緩存中的庫(kù)存扣減,并分配空閑場(chǎng)號(hào)** @param timePeriodReserveReqDTO* @param venueDO* @param stockKey* @param freeIndexBitMapKey* @return*/
private Long executeStockReduceByLua(TimePeriodReserveReqDTO timePeriodReserveReqDTO, VenueDO venueDO,String stockKey, String freeIndexBitMapKey) {// 使用 Hutool 的單例管理容器 管理lua腳本的加載,保證其只被加載一次String luaScriptPath = "lua/free_court_index_allocate_by_bitmap.lua";DefaultRedisScript<Long> luaScript = Singleton.get(luaScriptPath, () -> {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(luaScriptPath)));redisScript.setResultType(Long.class);return redisScript;});// 執(zhí)行用戶重復(fù)預(yù)定校驗(yàn)、庫(kù)存扣減、場(chǎng)號(hào)分配Long freeCourtIndex = stringRedisTemplate.execute(luaScript,Lists.newArrayList(String.format(stockKey, timePeriodReserveReqDTO.getTimePeriodId()),String.format(freeIndexBitMapKey, timePeriodReserveReqDTO.getTimePeriodId()),String.format(RedisCacheConstant.VENUE_IS_USER_BOUGHT_TIME_PERIOD_KEY, timePeriodReserveReqDTO.getTimePeriodId())),UserContext.getUserId().toString(),String.valueOf(venueDO.getAdvanceBookingDay() * 86400));return freeCourtIndex;
}

lua

-- 定義腳本參數(shù)
local stock_key = KEYS[1]
local free_index_bitmap_key = KEYS[2]
-- 用來(lái)存儲(chǔ)已購(gòu)買用戶的set
local set_name = KEYS[3]-- 用戶ID
local user_id = ARGV[1]
-- 過(guò)期時(shí)間 (秒)
local expire_time = tonumber(ARGV[2])-- 檢查用戶是否已經(jīng)購(gòu)買過(guò)
if redis.call("SISMEMBER", set_name, user_id) == 1 then-- 用戶已經(jīng)購(gòu)買過(guò),返回 -2 表示失敗return -2
end-- 獲取庫(kù)存
local current_inventory = tonumber(redis.call('GET', stock_key) or 0)-- 嘗試消耗庫(kù)存
if current_inventory < 1 then-- 庫(kù)存不夠了,返回-1,代表分配空?qǐng)鎏?hào)失敗return -1 -- 失敗
end-- 查找第一個(gè)空閑的場(chǎng)地(位圖中第一個(gè)為 0 的位)
local free_court_bit = redis.call("BITPOS", free_index_bitmap_key, 0)if not free_court_bit or free_court_bit == -1 then-- 沒有空閑的場(chǎng)號(hào)return -1 -- 失敗
end-- 占用該場(chǎng)地(將對(duì)應(yīng)位設(shè)置為 1)
redis.call("SETBIT", free_index_bitmap_key, free_court_bit, 1)
-- 更新庫(kù)存
redis.call('DECRBY', stock_key, 1)
-- 添加用戶到已購(gòu)買集合
redis.call("SADD", set_name, user_id)
-- 設(shè)置過(guò)期時(shí)間
if expire_time > 0 thenredis.call("EXPIRE", set_name, expire_time)
end-- 返回分配的場(chǎng)地索引(注意:位圖的位索引從0開始,如果你需要從1開始,這里加1)
return tonumber(free_court_bit)

【更新緩存中庫(kù)存】

大家可能會(huì)疑問,為啥有了令牌,還要更新緩存中的庫(kù)存和空閑場(chǎng)號(hào)。因?yàn)槲覀冊(cè)谇岸苏故镜男畔⑿枰钦鎸?shí)的庫(kù)存信息,為了加速查詢,需要將庫(kù)存緩存起來(lái),這里的緩存數(shù)據(jù)需要和數(shù)據(jù)庫(kù)一致。為了保證緩存和數(shù)據(jù)庫(kù)的最終一致性,可以開啟 binlog ,然后使用 Canal 進(jìn)行監(jiān)聽。如果數(shù)據(jù)庫(kù)中的數(shù)據(jù)更新了,就發(fā)送消息到消息隊(duì)列中,消費(fèi)消息時(shí)再更新緩存中的庫(kù)存。

-- 定義腳本參數(shù)
local stock_key = KEYS[1]
local free_index_bitmap_key = KEYS[2]-- 預(yù)訂場(chǎng)號(hào)
local free_court_bit = ARGV[1]-- 占用該場(chǎng)地(將對(duì)應(yīng)位設(shè)置為 1)
redis.call("SETBIT", free_index_bitmap_key, free_court_bit, 1)
-- 更新庫(kù)存
redis.call('DECRBY', stock_key, 1)return 0

【檢測(cè)和加載位圖緩存】

/*** 檢測(cè)位圖緩存是否加載好,沒有的話,執(zhí)行加載操作** @param freeIndexBitmapKey* @param timePeriodId* @param initStock*/
@Override
public void checkBitMapCache(String freeIndexBitmapKey, Long timePeriodId, int initStock) {String cache = stringRedisTemplate.opsForValue().get(freeIndexBitmapKey);if (StringUtils.isBlank(cache)) {// --if-- 如果緩存中的位圖為空RLock lock = redissonClient.getLock(String.format(RedisCacheConstant.VENUE_LOCK_TIME_PERIOD_FREE_INDEX_BIT_MAP_KEY, timePeriodId));lock.lock();try {// 雙重判定一下,避免其他線程已經(jīng)加載數(shù)據(jù)到緩存中了cache = stringRedisTemplate.opsForValue().get(freeIndexBitmapKey);if (StringUtils.isBlank(cache)) {// --if-- 如果緩存中的位圖還是空,到數(shù)據(jù)庫(kù)中加載位圖TimePeriodDO timePeriodDO = this.getById(timePeriodId);if (timePeriodDO == null) {throw new ServiceException(timePeriodId + "對(duì)應(yīng)的時(shí)間段為null", BaseErrorCode.SERVICE_ERROR);}// 將位圖信息設(shè)置到緩存中this.initializeFreeIndexBitmap(freeIndexBitmapKey, initStock, timePeriodDO.getBookedSlots(), 24 * 3600);}} finally {// 解鎖lock.unlock();}}
}/*** 初始化Redis中的位圖,并設(shè)置key的過(guò)期時(shí)間** @param freeIndexBitmapKey 位圖的鍵名* @param longValue          用于初始化位圖的 long 類型數(shù)據(jù)* @param expireSecond       key的過(guò)期時(shí)間(秒)*/
public void initializeFreeIndexBitmap(String freeIndexBitmapKey, int initStock, long longValue, long expireSecond) {// 將 long 轉(zhuǎn)換為64位的二進(jìn)制字符串String binaryString = Long.toBinaryString(longValue);// 確保字符串長(zhǎng)度為64位,不足的部分用0補(bǔ)齊binaryString = String.format("%64s", binaryString).replace(' ', '0');// 從低位到高位遍歷二進(jìn)制字符串,設(shè)置位圖中的對(duì)應(yīng)位for (int i = 0; i < 64 && initStock-- >= 0; i++) {// 注意:long的最低位對(duì)應(yīng)位圖的第0位if (binaryString.charAt(63 - i) == '1') {stringRedisTemplate.opsForValue().setBit(freeIndexBitmapKey, i, true).booleanValue();} else {stringRedisTemplate.opsForValue().setBit(freeIndexBitmapKey, i, false).booleanValue();}}// 設(shè)置過(guò)期時(shí)間,僅當(dāng)expireTime大于0時(shí)進(jìn)行設(shè)置if (expireSecond > 0) {stringRedisTemplate.expire(freeIndexBitmapKey, expireSecond, TimeUnit.SECONDS);}
}

【檢驗(yàn)和加載庫(kù)存緩存】

這里使用了封裝的緩存組件,需要去倉(cāng)庫(kù)查看詳細(xì)代碼

/*** 獲取指定時(shí)間段的庫(kù)存** @param timePeriodId* @return*/
@Override
public Integer getStockByTimePeriodId(Long timePeriodId) {return (Integer) distributedCache.safeGet(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_KEY, timePeriodId),new TypeReference<Integer>() {},() -> {TimePeriodDO timePeriodDO = this.getById(timePeriodId);return timePeriodDO.getStock();},1,TimeUnit.DAYS);
}

【消費(fèi)消息,執(zhí)行預(yù)訂流程】

和接口 V1 不同的是,V1 時(shí)同步創(chuàng)建訂單,創(chuàng)建完成之后,直接訪問給用戶訂單數(shù)據(jù)。但是在 V2 中,將任務(wù)交給消息隊(duì)列之后,就要返回成功了。用戶需要在前端等待訂單創(chuàng)建結(jié)果。那前端如何感知訂單是否創(chuàng)建成功呢?

  • 方式一:前端輪詢查詢后端,如每隔一秒問一下后端,訂單創(chuàng)建好沒有,創(chuàng)建好了就返回給前端,這樣前端就可以進(jìn)行支付了
  • 方式二:使用前后端雙向通訊技術(shù),如WebSocket。前后端一開始先建立好連接,等后端消費(fèi)消息,創(chuàng)建訂單成功之后,直接將訂單信息推送給前端
/*** 通過(guò)消息隊(duì)列執(zhí)行 時(shí)間段預(yù)定 邏輯* @param executeReserveMqDTO*/
@Override
public void mqExecutePreserve(ExecuteReserveMqDTO executeReserveMqDTO) {TimePeriodDO timePeriodDO = this.getTimePeriodDOById(executeReserveMqDTO.getTimePeriodId());// 編程式開啟事務(wù),減少事務(wù)粒度,避免長(zhǎng)事務(wù)的發(fā)生transactionTemplate.executeWithoutResult(status -> {try {// 扣減當(dāng)前時(shí)間段的庫(kù)存,修改空閑場(chǎng)信息baseMapper.updateStockAndBookedSlots(timePeriodDO.getId(), timePeriodDO.getPartitionId(), executeReserveMqDTO.getFreeCourtIndex());// 更新緩存中的庫(kù)存、位圖if (!isUseBinlog) {// --if-- 如果不使用binlog,需要手動(dòng)更新緩存// 首先檢測(cè)空閑場(chǎng)號(hào)緩存有沒有加載好,沒有的話進(jìn)行加載this.checkBitMapCache(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_KEY, executeReserveMqDTO.getTimePeriodId()),executeReserveMqDTO.getTimePeriodId(),partitionService.getPartitionDOById(timePeriodDO.getPartitionId()).getNum());// 其次檢測(cè)時(shí)間段庫(kù)存有沒有加載好,沒有的話進(jìn)行加載this.getStockByTimePeriodId(executeReserveMqDTO.getTimePeriodId());// 使用 Hutool 的單例管理容器 管理lua腳本的加載,保證其只被加載一次String luaScriptPath = "lua/inventory_update.lua";DefaultRedisScript<Long> luaScript = Singleton.get(luaScriptPath, () -> {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(luaScriptPath)));redisScript.setResultType(Long.class);return redisScript;});// 庫(kù)存扣減、場(chǎng)號(hào)占用stringRedisTemplate.execute(luaScript,Lists.newArrayList(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_KEY, executeReserveMqDTO.getTimePeriodId()),String.format(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_KEY, executeReserveMqDTO.getTimePeriodId())),executeReserveMqDTO.getFreeCourtIndex().toString());}// todo 需要實(shí)現(xiàn)binlog版本// 調(diào)用遠(yuǎn)程服務(wù)創(chuàng)建訂單OrderGenerateReqDTO orderGenerateReqDTO = OrderGenerateReqDTO.builder().timePeriodId(timePeriodDO.getId()).partitionId(timePeriodDO.getPartitionId()).periodDate(timePeriodDO.getPeriodDate()).beginTime(timePeriodDO.getBeginTime()).endTime(timePeriodDO.getEndTime()).courtIndex(executeReserveMqDTO.getFreeCourtIndex()).userId(executeReserveMqDTO.getUserId()).userName(executeReserveMqDTO.getUserName()).venueId(executeReserveMqDTO.getVenueId()).payAmount(timePeriodDO.getPrice()).build();Result<OrderDO> result;try {result = orderFeignService.generateOrder(orderGenerateReqDTO);if (result == null || !result.isSuccess()) {// --if-- 訂單生成失敗,拋出異常,上面的庫(kù)存扣減也會(huì)回退throw new ServiceException(BaseErrorCode.ORDER_GENERATE_ERROR);}} catch (Exception e) {// --if-- 訂單生成服務(wù)調(diào)用失敗// 恢復(fù)緩存中的信息this.restoreStockAndBookedSlotsCache(timePeriodDO.getId(),1L,executeReserveMqDTO.getFreeCourtIndex(),RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY,RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY);// todo 如果說(shuō)由于網(wǎng)絡(luò)原因,實(shí)際上訂單已經(jīng)創(chuàng)建成功了,但是因?yàn)槌瑫r(shí)訪問失敗,這里庫(kù)存卻回滾了,此時(shí)需要將訂單置為廢棄狀態(tài)(即刪除)// 發(fā)送一個(gè)短暫的延時(shí)消息(時(shí)間過(guò)長(zhǎng),用戶可能已經(jīng)支付),去檢查訂單是否生成,如果生成,將其刪除// 打印錯(cuò)誤堆棧信息e.printStackTrace();// 把錯(cuò)誤返回到前端throw new ServiceException(e.getMessage());}OrderDO orderDO = result.getData();// todo 使用 WebSocket 通知前端,訂單生成成功} catch (Exception ex) {status.setRollbackOnly();throw ex;}});
}

MQ

生產(chǎn)者

import cn.hutool.core.util.StrUtil;
import com.vrs.constant.RocketMqConstant;
import com.vrs.domain.dto.mq.ExecuteReserveMqDTO;
import com.vrs.templateMethod.AbstractCommonSendProduceTemplate;
import com.vrs.templateMethod.BaseSendExtendDTO;
import com.vrs.templateMethod.MessageWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.MessageConst;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;import java.util.UUID;/*** 執(zhí)行預(yù)訂流程 生產(chǎn)者** @Author dam* @create 2024/9/20 16:00*/
@Slf4j
@Component
public class ExecuteReserveProducer extends AbstractCommonSendProduceTemplate<ExecuteReserveMqDTO> {@Overrideprotected BaseSendExtendDTO buildBaseSendExtendParam(ExecuteReserveMqDTO messageSendEvent) {return BaseSendExtendDTO.builder().eventName("執(zhí)行時(shí)間段預(yù)定").keys(String.valueOf(messageSendEvent.getTimePeriodId())).topic(RocketMqConstant.VENUE_TOPIC).tag(RocketMqConstant.TIME_PERIOD_EXECUTE_RESERVE_TAG).sentTimeout(2000L).build();}@Overrideprotected Message<?> buildMessage(ExecuteReserveMqDTO messageSendEvent, BaseSendExtendDTO requestParam) {String keys = StrUtil.isEmpty(requestParam.getKeys()) ? UUID.randomUUID().toString() : requestParam.getKeys();return MessageBuilder.withPayload(new MessageWrapper(keys, messageSendEvent)).setHeader(MessageConst.PROPERTY_KEYS, keys).setHeader(MessageConst.PROPERTY_TAGS, requestParam.getTag()).build();}
}

消費(fèi)者

import com.vrs.annotation.Idempotent;
import com.vrs.constant.RocketMqConstant;
import com.vrs.domain.dto.mq.ExecuteReserveMqDTO;
import com.vrs.enums.IdempotentSceneEnum;
import com.vrs.service.TimePeriodService;
import com.vrs.templateMethod.MessageWrapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.annotation.SelectorType;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;/*** 執(zhí)行預(yù)訂流程 消費(fèi)者* @Author dam* @create 2024/9/20 21:30*/
@Slf4j(topic = RocketMqConstant.VENUE_TOPIC)
@Component
@RocketMQMessageListener(topic = RocketMqConstant.VENUE_TOPIC,consumerGroup = RocketMqConstant.VENUE_CONSUMER_GROUP + "-" + RocketMqConstant.TIME_PERIOD_EXECUTE_RESERVE_TAG,messageModel = MessageModel.CLUSTERING,// 監(jiān)聽tagselectorType = SelectorType.TAG,selectorExpression = RocketMqConstant.TIME_PERIOD_EXECUTE_RESERVE_TAG
)
@RequiredArgsConstructor
public class ExecuteReserveListener implements RocketMQListener<MessageWrapper<ExecuteReserveMqDTO>> {private final TimePeriodService timePeriodService;/*** 消費(fèi)消息的方法* 方法報(bào)錯(cuò)就會(huì)拒收消息** @param messageWrapper 消息內(nèi)容,類型和上面的泛型一致。如果泛型指定了固定的類型,消息體就是我們的參數(shù)*/@Idempotent(uniqueKeyPrefix = "time_period_execute_reserve:",key = "#messageWrapper.getMessage().getTimePeriodId()+''",scene = IdempotentSceneEnum.MQ,keyTimeout = 3600L)@SneakyThrows@Overridepublic void onMessage(MessageWrapper<ExecuteReserveMqDTO> messageWrapper) {// 開頭打印日志,平???Debug 看任務(wù)參數(shù),線上可報(bào)平安(比如消息是否消費(fèi),重新投遞時(shí)獲取參數(shù)等)log.info("[消費(fèi)者] 執(zhí)行時(shí)間段預(yù)定,時(shí)間段ID:{}", messageWrapper.getMessage().getTimePeriodId());timePeriodService.mqExecutePreserve(messageWrapper.getMessage());}
}

測(cè)試結(jié)果

在這里插入圖片描述

  1. 樣本數(shù)量:共有40,000個(gè)樣本,這表示在測(cè)試期間進(jìn)行了40,000次操作或請(qǐng)求。
  2. 響應(yīng)時(shí)間
    1. 平均值:1801毫秒,表示所有請(qǐng)求的平均響應(yīng)時(shí)間。
    2. 中位數(shù):1346毫秒,表示50%的請(qǐng)求響應(yīng)時(shí)間低于這個(gè)值。
    3. 90%百分位:2048毫秒,表示90%的請(qǐng)求響應(yīng)時(shí)間低于這個(gè)值。
    4. 95%百分位:3410毫秒,表示95%的請(qǐng)求響應(yīng)時(shí)間低于這個(gè)值。
    5. 99%百分位:15133毫秒,表示99%的請(qǐng)求響應(yīng)時(shí)間低于這個(gè)值。
    6. 最小值:15毫秒,表示最快的請(qǐng)求響應(yīng)時(shí)間。
    7. 最大值:22121毫秒,表示最慢的請(qǐng)求響應(yīng)時(shí)間。
  3. 異常率:0.22%,表示在所有請(qǐng)求中,有0.22%的請(qǐng)求出現(xiàn)了異常。
  4. 吞吐量:每秒可以處理1045.8個(gè)請(qǐng)求
  5. 網(wǎng)絡(luò)流量
    1. 接收速率:221.51 KB/sec,表示系統(tǒng)每秒接收的數(shù)據(jù)量。
    2. 發(fā)送速率:509.96 KB/sec,表示系統(tǒng)每秒發(fā)送的數(shù)據(jù)量。

總結(jié)

  • 系統(tǒng)的平均響應(yīng)時(shí)間為1801毫秒,中位數(shù)為1346毫秒,表明大多數(shù)請(qǐng)求的響應(yīng)時(shí)間在可接受范圍內(nèi)。
  • 99%的請(qǐng)求響應(yīng)時(shí)間在15133毫秒以內(nèi),但有少數(shù)請(qǐng)求的響應(yīng)時(shí)間較長(zhǎng),最大值達(dá)到了22121毫秒。
  • 系統(tǒng)的吞吐量為1045.8次請(qǐng)求/秒,處理能力較高,相較于接口V1,性能強(qiáng)了一倍

說(shuō)明

文章內(nèi)容并非最新代碼實(shí)現(xiàn),若需要知道最新實(shí)現(xiàn),麻煩移步開源倉(cāng)庫(kù): HelloDam/場(chǎng)快訂(場(chǎng)館預(yù)定 SaaS 平臺(tái))

http://www.risenshineclean.com/news/55425.html

相關(guān)文章:

  • 同性男做性視頻網(wǎng)站seo小白入門教學(xué)
  • 醫(yī)院做網(wǎng)站定位上海seo網(wǎng)站策劃
  • 做平臺(tái)網(wǎng)站一般有php還是js脫發(fā)嚴(yán)重是什么原因引起的
  • 網(wǎng)站正在建設(shè)中代碼百度賬號(hào)中心官網(wǎng)
  • 0511城市建設(shè)網(wǎng)站棚戶區(qū)改造seo關(guān)鍵詞推廣案例
  • 文本怎樣做閱讀鏈接網(wǎng)站自媒體怎么做
  • 外掛網(wǎng)站怎么做廣告代理商
  • 營(yíng)銷網(wǎng)站建設(shè)平臺(tái)淘寶店鋪推廣
  • 網(wǎng)站優(yōu)化都是怎么做的百度信息流效果怎么樣
  • 門戶網(wǎng)站 模板之家網(wǎng)站推廣方式有哪些
  • 利用網(wǎng)站空間做代理seo推廣一年要多少錢
  • 做商務(wù)網(wǎng)站要多少錢html網(wǎng)頁(yè)制作軟件有哪些
  • 自己做的網(wǎng)站很卡網(wǎng)絡(luò)營(yíng)銷方案設(shè)計(jì)范文
  • 桂林漓江門票多少錢一張seo軟件推廣哪個(gè)好
  • wordpress如何修改評(píng)論長(zhǎng)沙seo關(guān)鍵詞
  • wordpress做自建站app接入廣告變現(xiàn)
  • 在wordpress主題后臺(tái)安裝了多說(shuō)插件但網(wǎng)站上顯示不出評(píng)論模塊競(jìng)價(jià)廣告是什么意思
  • 滄州做網(wǎng)站的公司營(yíng)銷方案包括哪些內(nèi)容
  • 對(duì)網(wǎng)站建設(shè)服務(wù)公司的看法新東方烹飪學(xué)校學(xué)費(fèi)一年多少錢
  • 各種瀏覽器網(wǎng)站大全淘寶新店怎么快速做起來(lái)
  • 互聯(lián)網(wǎng)網(wǎng)站建設(shè)公司做個(gè)電商平臺(tái)要多少錢
  • APP開發(fā)網(wǎng)站建設(shè)哪家好免費(fèi)網(wǎng)站統(tǒng)計(jì)代碼
  • 深圳wap網(wǎng)站建設(shè)公司關(guān)鍵詞優(yōu)化公司推薦
  • 怎么做色情網(wǎng)站賺錢品牌推廣策劃書范文案例
  • 建設(shè)政府信息資源共享網(wǎng)站如何查詢百度收錄情況
  • 中山企業(yè)網(wǎng)站制作寧德市委書記
  • 聊城做網(wǎng)站的公司信息市場(chǎng)調(diào)研報(bào)告怎么寫的
  • 做京東網(wǎng)站需要哪些手續(xù)互聯(lián)網(wǎng)宣傳推廣
  • 怎么把網(wǎng)站的標(biāo)題做的炫酷網(wǎng)絡(luò)營(yíng)銷工程師
  • 常州網(wǎng)站制作報(bào)價(jià)故事式軟文廣告300字