Redis解決優(yōu)惠券秒殺應(yīng)用案例
雖然本文是針對黑馬點評的優(yōu)惠券秒殺業(yè)務(wù)的實現(xiàn),但是是適用于各種搶購活動,保證線程安全。
摘要:本文先講了搶購問題,指出其中會出現(xiàn)的多線程問題,提出解決方案采用悲觀鎖和樂觀鎖兩種方式進行實現(xiàn),然后發(fā)現(xiàn)在搶購過程中容易出現(xiàn)一人多單現(xiàn)象,為保證優(yōu)惠券不會被【黃?!繐尩剑虼宋覀冊?strong>保證多線程安全的情況下實現(xiàn)了一人一單業(yè)務(wù),最后指出本文的實現(xiàn)在集群情況下的不足之處。在本專欄的另一篇文章中提出集群或者分布式系統(tǒng)的解決方案。
【前端頁面】

在代金券發(fā)放后,多個用戶會進行優(yōu)惠券搶購,在搶購時需要判斷兩點:
下單時需要判斷兩點:
- 秒殺是否開始或結(jié)束,如果尚未開始或已經(jīng)結(jié)束則無法下單
- 庫存是否充足,不足則無法下單
下單核心邏輯分析:
當用戶開始進行下單,我們應(yīng)當去查詢優(yōu)惠卷信息,查詢到優(yōu)惠卷信息,判斷是否滿足秒殺條件
比如時間是否充足,如果時間充足,則進一步判斷庫存是否足夠,如果兩者都滿足,則扣減庫存,創(chuàng)建訂單,然后返回訂單id,如果有一個條件不滿足則直接結(jié)束。
【邏輯圖】

【代碼實現(xiàn)】
@Override
public Result seckillVoucher(Long voucherId) {
// 1.查詢優(yōu)惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判斷秒殺是否開始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未開始
return Result.fail("秒殺尚未開始!");
}
// 3.判斷秒殺是否已經(jīng)結(jié)束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未開始
return Result.fail("秒殺已經(jīng)結(jié)束!");
}
// 4.判斷庫存是否充足#######
if (voucher.getStock() < 1) {
// 庫存不足
return Result.fail("庫存不足!");
}
//5,扣減庫存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣減庫存
return Result.fail("庫存不足!");
}
//6.創(chuàng)建訂單
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1.訂單id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 6.2.用戶id
Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
// 6.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}【分析代碼】
- 從上述的邏輯圖中我們可以知道,要扣減庫存,并且要保存訂單,因此需要事務(wù)業(yè)務(wù)
- 在第4步判斷庫存是否充足處,會出現(xiàn)多線程問題。出現(xiàn)訂單超賣現(xiàn)象
問題代碼如下:
if (voucher.getStock() < 1) {
// 庫存不足
return Result.fail("庫存不足!");
}
//5,扣減庫存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣減庫存
return Result.fail("庫存不足!");
}

【采用鎖】解決上述超賣問題。
悲觀鎖:
悲觀鎖可以實現(xiàn)對于數(shù)據(jù)的串行化執(zhí)行,比如syn,和lock都是悲觀鎖的代表,同時,悲觀鎖中又可以再細分為公平鎖,非公平鎖,可重入鎖,等等
樂觀鎖:
樂觀鎖:會有一個版本號,每次操作數(shù)據(jù)會對版本號+1,再提交回數(shù)據(jù)時,會去校驗是否比之前的版本大1 ,如果大1 ,則進行操作成功,這套機制的核心邏輯在于,如果在操作過程中,版本號只比原來大1 ,那么就意味著操作過程中沒有人對他進行過修改,他的操作就是安全的,如果不大1,則數(shù)據(jù)被修改過,當然樂觀鎖還有一些變種的處理方式比如cas
樂觀鎖的典型代表:就是cas,利用cas進行無鎖化機制加鎖,var5 是操作前讀取的內(nèi)存值,while中的var1+var2 是預(yù)估值,如果預(yù)估值 == 內(nèi)存值,則代表中間沒有被人修改過,此時就將新值去替換 內(nèi)存值
其中do while 是為了在操作失敗時,再次進行自旋操作,即把之前的邏輯再操作一次。
修改代碼方案
我們的樂觀鎖保證stock大于0 即可,如果查詢邏輯stock不能保證大于0,則會出現(xiàn) success為false我們在后文進行判斷即可。
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0
if (!success) {
//扣減庫存
return Result.fail("庫存不足!");
}
代碼寫到這里,我們就解決了多線程安全問題(優(yōu)惠券超賣)
一人一單
但是我們在檢查數(shù)據(jù)庫數(shù)據(jù)時,我們發(fā)現(xiàn)一個人可以購買多個優(yōu)惠券。
因此我們可以在搶購前,判斷該用戶是否已經(jīng)購買過該優(yōu)惠券,如果購買過則直接返回。
【邏輯圖】紅框內(nèi)的是新增邏輯。

@Override
public Result seckillVoucher(Long voucherId) {
// 1.查詢優(yōu)惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判斷秒殺是否開始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未開始
return Result.fail("秒殺尚未開始!");
}
// 3.判斷秒殺是否已經(jīng)結(jié)束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 尚未開始
return Result.fail("秒殺已經(jīng)結(jié)束!");
}
// 4.判斷庫存是否充足
if (voucher.getStock() < 1) {
// 庫存不足
return Result.fail("庫存不足!");
}
// 5.一人一單邏輯
// 5.1.用戶id
Long userId = UserHolder.getUser().getId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判斷是否存在
if (count > 0) {
// 用戶已經(jīng)購買過了
return Result.fail("用戶已經(jīng)購買過一次!");
}
//6,扣減庫存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣減庫存
return Result.fail("庫存不足!");
}
//7.創(chuàng)建訂單
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.訂單id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
return Result.ok(orderId);
}【分析代碼】---仍然會出現(xiàn)多線程問題。
存在問題:現(xiàn)在的問題還是和之前一樣,并發(fā)過來,查詢數(shù)據(jù)庫,都不存在訂單,所以我們還是需要加鎖,但是樂觀鎖比較適合更新數(shù)據(jù),而現(xiàn)在是插入數(shù)據(jù),所以我們需要使用悲觀鎖操作
【注意事項】
- 事務(wù)應(yīng)該包含在鎖的內(nèi)部。
- 鎖的粒度,鎖的對象應(yīng)該是用戶級別的,而不是整個搶購優(yōu)惠券級別的,因此我們不會直接將synchronized加到方法上。
- 鎖對象的細節(jié)處理,使用userId.toString().intern()保證對象唯一。
- 獲取代理對象調(diào)用切入事務(wù)
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.RedisWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* <p>
* 服務(wù)實現(xiàn)類
* </p>
*
* @author msf
* @since 2022-10-29
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisWorker redisWorker;
@Override
public Result seckillVoucher(Long voucherId) {
// 1. 查詢優(yōu)惠券信息
SeckillVoucher voucherOrder = seckillVoucherService.getById(voucherId);
// 2.判斷秒殺是否開始
if (voucherOrder.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("搶購尚未開始");
}
if (voucherOrder.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("搶購已經(jīng)結(jié)束");
}
// 3.判斷庫存是否充足
if (voucherOrder.getStock() < 1) {
return Result.fail("您來晚了,票已被搶完");
}
Long userId = UserHolder.getUser().getId();
// 事務(wù)應(yīng)該在synchronized里面
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId,userId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId,Long userId) {
// 4. 一人一單邏輯
// 4.1 根據(jù)優(yōu)惠券id和用戶id查詢訂單
Integer count = query().eq("user_id", userId)
.eq("voucher_id", voucherId).count();
// 4.2 訂單存在,直接返回
if (count > 0) {
return Result.fail("用戶已經(jīng)購買一次");
}
// 5. 扣減庫存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.gt("stock", 0)
.eq("voucher_id", voucherId).update();
if (!success) {
return Result.fail("庫存不足");
}
// 6.創(chuàng)建訂單
VoucherOrder order = new VoucherOrder();
// 6.1 設(shè)置id
order.setId(redisWorker.nextId("order"));
// 6.2 設(shè)置訂單id
order.setVoucherId(voucherId);
// 6.3 設(shè)置用戶id
order.setUserId(userId);
save(order);
// 7. 返回訂單id
return Result.ok(order);
}
}展望
雖然我們利用鎖和事務(wù)解決單體系統(tǒng)下的秒殺功能,但是現(xiàn)在的業(yè)務(wù)一般是在集群和分布式系統(tǒng)協(xié)作完成,因此我們在測試系統(tǒng)在集群部署時,仍會出現(xiàn)一人多單問題,稍后我們將更新文章,分析問題出現(xiàn)原因,并利用分布式鎖的方式解決該問題。
到此這篇關(guān)于Redis解決優(yōu)惠券秒殺的文章就介紹到這了,更多相關(guān)Redis優(yōu)惠券秒殺內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis源碼與設(shè)計剖析之網(wǎng)絡(luò)連接庫
這篇文章主要為大家介紹了Redis源碼與設(shè)計剖析之網(wǎng)絡(luò)連接庫詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09
Redis集群增加節(jié)點與刪除節(jié)點的方法詳解
這篇文章主要給大家介紹了關(guān)于Redis集群增加節(jié)點與刪除節(jié)點的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Redis具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-09-09

