Redis優(yōu)惠券秒殺解決方案
1 實(shí)現(xiàn)優(yōu)惠券秒殺功能
下單時(shí)需要判斷兩點(diǎn):1.秒殺是否開(kāi)始或者結(jié)束2.庫(kù)存是否充足
所以,我們的業(yè)務(wù)邏輯如下
1. 通過(guò)優(yōu)惠券id獲取優(yōu)惠券信息
2.判斷秒殺是否開(kāi)始,如果未返回錯(cuò)誤信息
3.判斷秒殺是否結(jié)束,如果已經(jīng)結(jié)束返回錯(cuò)誤信息
4.如果在秒殺時(shí)間內(nèi),判斷庫(kù)存是否充足
5.如果充足,扣減庫(kù)存
6.創(chuàng)建訂單信息,并保存到優(yōu)惠券訂單表中
6.1 保存訂單id
6.2保存用戶(hù)id
6.3保存優(yōu)惠券id
7.返回訂單id
代碼實(shí)現(xiàn):(Service層實(shí)現(xiàn)類(lèi))
package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWorker; import com.hmdp.utils.UserHolder; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDateTime; /** * <p> * 服務(wù)實(shí)現(xiàn)類(lèi) * </p> * * @author 虎哥 * @since 2021-12-22 */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService iSeckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { //1.獲取優(yōu)惠券信息 SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId); //2.判斷是否已經(jīng)開(kāi)始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ Result.fail("秒殺尚未開(kāi)始!"); } //3.判斷是否已經(jīng)結(jié)束 if (voucher.getEndTime().isBefore(LocalDateTime.now())){ Result.fail("秒殺已經(jīng)結(jié)束了!"); } //4.判斷庫(kù)存是否充足 if (voucher.getStock() < 1) { Result.fail("庫(kù)存不充足!"); } //5.扣減庫(kù)存 boolean success = iSeckillVoucherService.update() .setSql("stock = stock-1").eq("voucher_id",voucherId) .update(); if (!success){ Result.fail("庫(kù)存不充足!"); } //6. 創(chuàng)建訂單 VoucherOrder voucherOrder = new VoucherOrder(); //6.1添加訂單id Long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //6.2添加用戶(hù)id Long userId = UserHolder.getUser().getId(); voucherOrder.setUserId(userId); //6.3添加優(yōu)惠券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //7.返回訂單id return Result.ok(orderId); } }
2 超賣(mài)問(wèn)題(重點(diǎn))
我們先嘗試在高并發(fā)的情況下運(yùn)行上述代碼。(使用jmx工具)
下圖是創(chuàng)建了兩百個(gè)線(xiàn)程,在一瞬間發(fā)出優(yōu)惠券請(qǐng)求
但是我們看聚合報(bào)告,發(fā)現(xiàn)異常值只有45.5%,按道理來(lái)說(shuō)應(yīng)該是50%(因?yàn)閹?kù)存有100個(gè),這里發(fā)出了200個(gè)請(qǐng)求)
一看庫(kù)存數(shù),好家伙,是-9
訂單也是添加了109個(gè),這顯然發(fā)生了超賣(mài)的問(wèn)題。
那么,為什么會(huì)發(fā)生這種問(wèn)題呢?
看圖說(shuō)話(huà):
按照我們正常的流程來(lái)走,就是線(xiàn)程1線(xiàn)查詢(xún)完庫(kù)存,然后扣減庫(kù)存,這個(gè)時(shí)候線(xiàn)程2再來(lái)查詢(xún)庫(kù)存,扣減庫(kù)存,這樣是沒(méi)問(wèn)題的。
超賣(mài)的問(wèn)題就出在,在訂單1查詢(xún)庫(kù)存后,發(fā)現(xiàn)是1,但還沒(méi)去扣減的時(shí)候,線(xiàn)程2也來(lái)查詢(xún)庫(kù)存,發(fā)現(xiàn)也是1,也進(jìn)行了扣減(高并發(fā)的場(chǎng)景下)
這就導(dǎo)致了超賣(mài)的問(wèn)題。
對(duì)于這種高并發(fā)的問(wèn)題,最常見(jiàn)的解決方法就是:上鎖~
但鎖又包括悲觀(guān)鎖和樂(lè)觀(guān)鎖。
悲觀(guān)鎖簡(jiǎn)單的講就是:覺(jué)得線(xiàn)程一定會(huì)發(fā)生,然后在操作之前每個(gè)人先拿鎖,你執(zhí)行完后,在輪到下一個(gè)來(lái)執(zhí)行(串行執(zhí)行)
樂(lè)觀(guān)鎖 :就是樂(lè)觀(guān)(認(rèn)為線(xiàn)程安全一定不會(huì)發(fā)生),只要在每次對(duì)數(shù)據(jù)修改之前,判斷其他線(xiàn)程是否對(duì)數(shù)據(jù)進(jìn)行的修改來(lái)保證線(xiàn)程安全。
悲觀(guān)鎖較為簡(jiǎn)單,這里實(shí)現(xiàn)樂(lè)觀(guān)鎖。
樂(lè)觀(guān)鎖的關(guān)鍵是判斷之前查詢(xún)得到的數(shù)據(jù)是否有被修改過(guò),常見(jiàn)的方式有兩種
溫馨提示:左邊表格的數(shù)據(jù)都是線(xiàn)程1執(zhí)行后的數(shù)據(jù)哦~
1.版本號(hào)法
就是在查詢(xún)庫(kù)存的步驟上加上一個(gè)版本號(hào),每次修改完數(shù)據(jù)后給版本號(hào)+1并在后面加上where條件判斷版本號(hào)是否和修改前的一致
這樣就可以做到線(xiàn)程安全啦~
2.CAS法
這個(gè)就是不用版本號(hào)了,直接在修改數(shù)據(jù)庫(kù)后加上where條件判斷庫(kù)存是否是修改前的庫(kù)存
解決超賣(mài)問(wèn)題代碼實(shí)現(xiàn):
說(shuō)到底就是在我們扣減庫(kù)存的時(shí)候加上一個(gè)where條件判斷庫(kù)存是否大于0
//5.1扣減庫(kù)存 boolean success = iSeckillVoucherService.update() .setSql("stock = stock-1").eq("voucher_id" , voucherId).gt("stock" ,0) .update();
package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.VoucherOrderMapper; import com.hmdp.service.ISeckillVoucherService; import com.hmdp.service.IVoucherOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmdp.utils.RedisIdWorker; import com.hmdp.utils.UserHolder; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDateTime; /** * <p> * 服務(wù)實(shí)現(xiàn)類(lèi) * </p> */ @Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService { @Resource private ISeckillVoucherService iSeckillVoucherService; @Resource private RedisIdWorker redisIdWorker; @Override public Result seckillVoucher(Long voucherId) { //1.獲取優(yōu)惠券信息 SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId); //2.判斷是否已經(jīng)開(kāi)始 if (voucher.getBeginTime().isAfter(LocalDateTime.now())){ Result.fail("秒殺尚未開(kāi)始!"); } //3.判斷是否已經(jīng)結(jié)束 if (voucher.getEndTime().isBefore(LocalDateTime.now())){ Result.fail("秒殺已經(jīng)結(jié)束了!"); } //4.判斷庫(kù)存是否充足 if (voucher.getStock() < 1) { Result.fail("庫(kù)存不充足!"); } //5.扣減庫(kù)存 boolean success = iSeckillVoucherService.update() .setSql("stock = stock-1").eq("voucher_id",voucherId).gt("stock",0) .update(); if (!success){ Result.fail("庫(kù)存不充足!"); } //6. 創(chuàng)建訂單 VoucherOrder voucherOrder = new VoucherOrder(); //6.1添加訂單id Long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); //6.2添加用戶(hù)id Long userId = UserHolder.getUser().getId(); voucherOrder.setUserId(userId); //6.3添加優(yōu)惠券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); //7.返回訂單id return Result.ok(orderId); } }
超賣(mài)問(wèn)題解決
到此這篇關(guān)于Redis優(yōu)惠券秒殺解決方案的文章就介紹到這了,更多相關(guān)Redis優(yōu)惠券秒殺內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
從零搭建SpringBoot2.X整合Redis框架的詳細(xì)教程
這篇文章主要介紹了從零搭建SpringBoot2.X整合Redis框架的詳細(xì)教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12Redis中的String類(lèi)型及使用Redis解決訂單秒殺超賣(mài)問(wèn)題
這篇文章主要介紹了Redis中的String類(lèi)型及使用Redis解決訂單秒殺超賣(mài)問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11關(guān)于使用IDEA的springboot框架往Redis里寫(xiě)入數(shù)據(jù)亂碼問(wèn)題
這篇文章主要介紹了用IDEA的springboot框架往Redis里寫(xiě)入數(shù)據(jù)亂碼問(wèn)題,本文給大家分享解決方法通過(guò)圖文示例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10利用Redis進(jìn)行數(shù)據(jù)緩存的項(xiàng)目實(shí)踐
在實(shí)際的業(yè)務(wù)場(chǎng)景中,Redis 一般和其他數(shù)據(jù)庫(kù)搭配使用,用來(lái)減輕后端數(shù)據(jù)庫(kù)的壓力,本文就介紹了利用Redis進(jìn)行數(shù)據(jù)緩存的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2022-06-06解析高可用Redis服務(wù)架構(gòu)分析與搭建方案
我們按照由簡(jiǎn)至繁的步驟,搭建一個(gè)最小型的高可用的Redis服務(wù)。 本文通過(guò)四種方案給大家介紹包含每種方案的優(yōu)缺點(diǎn)及詳細(xì)解說(shuō),具體內(nèi)容詳情跟隨小編一起看看吧2021-06-06