Spring?Boot?整合Redis?實(shí)現(xiàn)優(yōu)惠卷秒殺?一人一單功能
一、什么是全局唯一ID
?全局唯一ID
在分布式系統(tǒng)中,經(jīng)常需要使用全局唯一ID查找對(duì)應(yīng)的數(shù)據(jù)。產(chǎn)生這種ID需要保證系統(tǒng)全局唯一,而且要高性能以及占用相對(duì)較少的空間。
全局唯一ID在數(shù)據(jù)庫(kù)中一般會(huì)被設(shè)成主鍵,這樣為了保證數(shù)據(jù)插入時(shí)索引的快速建立,還需要保持一個(gè)有序的趨勢(shì)。
這樣全局唯一ID就需要保證這兩個(gè)需求:
- 全局唯一
- 趨勢(shì)有序
我們的場(chǎng)景是 優(yōu)惠卷秒殺搶購(gòu), 當(dāng)用戶(hù)搶購(gòu)時(shí),就會(huì)生成訂單 并保存到 數(shù)據(jù)庫(kù) 的訂單表中,而訂單表 如果使用數(shù)據(jù)庫(kù)自增ID就會(huì)存在以下問(wèn)題
- id的規(guī)律性太明顯
- 受單表數(shù)據(jù)量限制
場(chǎng)景分析:如果我們的id具有太明顯的規(guī)則,用戶(hù)或者說(shuō)商業(yè)對(duì)手很容易猜測(cè)出來(lái)我們的一些敏感信息,比如商城在一天時(shí)間內(nèi),賣(mài)出了多少單,這明顯不合適。
場(chǎng)景分析二: 隨著我們商城規(guī)模越來(lái)越大,MySQL 的單表的容量不宜超過(guò)500W,數(shù)據(jù)量過(guò)大之后,我們要進(jìn)行拆庫(kù)拆表,但拆分表了之后,他們從邏輯上講他們是同一張表,所以他們的id是不能一樣的, 于是乎我們需要保證id的唯一性。
全局ID生成器,是一種在分布式系統(tǒng)下用來(lái)生成全局唯一ID的工具,一般要滿(mǎn)足下列特性:

為了增加ID的安全性,我們可以不直接使用Redis自增的數(shù)值,而是拼接一些其它信息:

ID的組合為
- 符號(hào)位: 1bit,永遠(yuǎn)為0
- 時(shí)間戳: 31bit,以秒為單位可以使用69年
- 序列號(hào): 32bit,秒內(nèi)的計(jì)數(shù)器,支持每秒產(chǎn)生
2^32個(gè) 不同ID
?Redis實(shí)現(xiàn)全局唯一ID
編寫(xiě)工具類(lèi)
@Component
public class RedisIdWorker {
/**
* 開(kāi)始時(shí)間戳
*/
private static final long BEGIN_TIMESTAMP = 1640995200L;
/**
* 序列號(hào)的位數(shù)
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成時(shí)間戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列號(hào)
// 2.1.獲取當(dāng)前日期,精確到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增長(zhǎng)
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
測(cè)試存入Redis
@Autowired
private RedisIdWorker redisIdWorker;
private ExecutorService es = Executors.newFixedThreadPool(500);
@Test
public void testWorkerId() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(300);
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
long id = redisIdWorker.nextId("order");
System.out.println("id = " + id);
}
latch.countDown();
};
long begin = System.currentTimeMillis();
for (int i = 0; i < 300; i++) {
es.submit(task);
}
latch.await();
long end = System.currentTimeMillis();
System.out.println("times = " + (end- begin));
}
這里用到了 CountDownlatch,簡(jiǎn)單的介紹一下:
CountDownLatch名為信號(hào)槍?zhuān)褐饕淖饔檬?strong>同步協(xié)調(diào)在多線(xiàn)程的等待于喚醒問(wèn)題
我們?nèi)绻麤](méi)有CountDownLatch ,那么由于程序是異步的,當(dāng)異步程序沒(méi)有執(zhí)行完時(shí),主線(xiàn)程就已經(jīng)執(zhí)行完了,然后我們期望的是分線(xiàn)程全部走完之后,主線(xiàn)程再走,所以我們此時(shí)需要使用到CountDownLatch
CountDownLatch 中有兩個(gè)最重要的方法
- countDown
- await
await 是阻塞方法,我們擔(dān)心線(xiàn)程沒(méi)有執(zhí)行完時(shí),main線(xiàn)程就執(zhí)行,所以可以使用await就阻塞主線(xiàn)程, 那么什么時(shí)候main線(xiàn)程不在阻塞呢? 當(dāng) CountDownLatch 內(nèi)部維護(hù)的變量為0時(shí),就不再阻塞,直接放行。
什么時(shí)候 CountDownLatch 維護(hù)的變量變?yōu)? 呢,我們只需要調(diào)用一次countDown ,內(nèi)部變量就減少1,我們讓分線(xiàn)程和變量綁定, 執(zhí)行完一個(gè)分線(xiàn)程就減少一個(gè)變量,當(dāng)分線(xiàn)程全部走完,CountDownLatch 維護(hù)的變量就是0,此時(shí)await就不再阻塞,統(tǒng)計(jì)出來(lái)的時(shí)間也就是所有分線(xiàn)程執(zhí)行完后的時(shí)間。
二、環(huán)境準(zhǔn)備
需要搭建登錄環(huán)境,基礎(chǔ)環(huán)境代碼和sql文件均已上傳 GitCode 鏈接:基礎(chǔ)環(huán)境和SQL
三、實(shí)現(xiàn)秒殺下單
添加優(yōu)惠卷
VoucherServiceImpl 核心代碼
@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {
// 該類(lèi)無(wú)代碼,直接MyBatis-Plus繼承實(shí)現(xiàn)類(lèi) 即可,自動(dòng)完成持久化
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Override
public ResultBean<List<Voucher>> queryVoucherOfShop(Long shopId) {
// 查詢(xún)優(yōu)惠券信息
List<Voucher> vouchers = getBaseMapper().queryVoucherOfShop(shopId);
// 返回結(jié)果
return ResultBean.create(0, "success", vouchers);
}
@Override
public void addSeckillVoucher(Voucher voucher) {
// 保存優(yōu)惠券
save(voucher);
// 保存秒殺信息
SeckillVoucher seckillVoucher = new SeckillVoucher();
seckillVoucher.setVoucherId(voucher.getId());
seckillVoucher.setStock(voucher.getStock());
seckillVoucher.setBeginTime(voucher.getBeginTime());
seckillVoucher.setEndTime(voucher.getEndTime());
seckillVoucherService.save(seckillVoucher);
}
}VoucherController 接口層
@RestController
@CrossOrigin
@RequestMapping("/voucher")
public class VoucherController {
@Autowired
private IVoucherService voucherService;
/**
* 新增秒殺券
* @param voucher 優(yōu)惠券信息,包含秒殺信息
* @return 優(yōu)惠券id
*/
@PostMapping("seckill")
public ResultBean addSeckillVoucher(@RequestBody Voucher voucher) {
voucherService.addSeckillVoucher(voucher);
return Result.ok(voucher.getId());
}
}
編寫(xiě)下單業(yè)務(wù)
VoucherOrderServiceImpl 優(yōu)惠卷訂單核心業(yè)務(wù)類(lèi)
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//1. 查詢(xún)優(yōu)惠卷
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2. 判斷秒殺是否開(kāi)始 開(kāi)始時(shí)間大于當(dāng)前時(shí)間表示未開(kāi)始搶購(gòu)
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒殺尚未開(kāi)始!");
}
//3. 判斷秒殺是否結(jié)束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒殺已經(jīng)結(jié)束!");
}
//4. 判斷庫(kù)存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("庫(kù)存不足!");
}
Long userId = UserHolder.getUser().getId();
//5. 查詢(xún)訂單
//5.1 查詢(xún)訂單
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2 判斷并返回
if (count > 0) {
return Result.fail("用戶(hù)已經(jīng)購(gòu)買(mǎi)過(guò)!");
}
//6. 扣減庫(kù)存
boolean success = seckillVoucherService.update().setSql("stock = stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
return Result.fail("庫(kù)存不足!");
}
//7. 創(chuàng)建訂單
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8. 返回訂單id
return Result.ok(orderId);
}
}
VoucherOrderController 接口層
@RestController
@CrossOrigin
@RequestMapping("/voucher_order")
public class VoucherOrderController {
@Autowired
private IVoucherOrderService voucherOrderService;
@PostMapping("seckill/{id}")
public Result seckillVoucher(@PathVariable("id") Long voucherId) {
return voucherOrderService.seckillVoucher(voucherId);
}
}測(cè)試搶購(gòu)秒殺優(yōu)惠卷
ApiFox 新增以下接口
添加秒殺卷

測(cè)試返回成功即可。
搶購(gòu)秒殺優(yōu)惠卷接口

測(cè)試無(wú)誤,搶購(gòu)成功!
四、庫(kù)存超賣(mài)問(wèn)題
?問(wèn)題分析
有關(guān)超賣(mài)問(wèn)題分析:在我們?cè)写a中是這么寫(xiě)的
if (voucher.getStock() < 1) {
// 庫(kù)存不足
return Result.fail("庫(kù)存不足!");
}
//5,扣減庫(kù)存
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update();
if (!success) {
//扣減庫(kù)存
return Result.fail("庫(kù)存不足!");
}
假設(shè)線(xiàn)程1過(guò)來(lái)查詢(xún)庫(kù)存,判斷出來(lái)庫(kù)存大于1,正準(zhǔn)備去扣減庫(kù)存,但是還沒(méi)有來(lái)得及去扣減,此時(shí)線(xiàn)程2過(guò)來(lái),線(xiàn)程2也去查詢(xún)庫(kù)存,發(fā)現(xiàn)這個(gè)數(shù)量一定也大于1,那么這兩個(gè)線(xiàn)程都會(huì)去扣減庫(kù)存,最終多個(gè)線(xiàn)程相當(dāng)于一起去扣減庫(kù)存,此時(shí)就會(huì)出現(xiàn)庫(kù)存的超賣(mài)問(wèn)題。

超賣(mài)問(wèn)題是典型的多線(xiàn)程安全問(wèn)題, 這種情況下常見(jiàn)的解決方案就是 加 鎖:而對(duì)于加鎖,我們通常有兩種解決方案:

悲觀(guān)鎖:
悲觀(guān)鎖可以實(shí)現(xiàn)對(duì)于數(shù)據(jù)的串行化執(zhí)行,比如syn,和lock都是悲觀(guān)鎖的代表,同時(shí),悲觀(guān)鎖中又可以再細(xì)分為公平鎖,非公平鎖,可重入鎖,等等
樂(lè)觀(guān)鎖:
會(huì)有一個(gè)版本號(hào),每次操作數(shù)據(jù)會(huì)對(duì)版本號(hào)+1,再提交回?cái)?shù)據(jù)時(shí),會(huì)去校驗(yàn)是否比之前的版本大1 ,如果大1 ,則進(jìn)行操作成功,這套機(jī)制的核心邏輯在于,**如果在操作過(guò)程中,版本號(hào)只比原來(lái)大1 ,那么就意味著操作過(guò)程中沒(méi)有人對(duì)他進(jìn)行過(guò)修改,他的操作就是安全的,**如果不大1,則數(shù)據(jù)被修改過(guò),當(dāng)然樂(lè)觀(guān)鎖還有一些變種的處理方式比如cas
樂(lè)觀(guān)鎖的典型代表:就是CAS,利用CAS進(jìn)行無(wú)鎖化機(jī)制加鎖,varNum是操作前讀取的內(nèi)存值,while中的var1+var2 是預(yù)估值,如果預(yù)估值 == 內(nèi)存值,則代表中間沒(méi)有被人修改過(guò),此時(shí)就將新值去替換 內(nèi)存值
其中do while 是為了在操作失敗時(shí),再次進(jìn)行自旋操作,即把之前的邏輯再操作一次。
int varNum;
do {
varNum = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
我們采用的方式為:
在操作時(shí),對(duì)版本號(hào)進(jìn)行+1 操作,然后要求version 如果是1 的情況下,才能操作,那么第一個(gè)線(xiàn)程在操作后,數(shù)據(jù)庫(kù)中的version變成了2,但是他自己滿(mǎn)足version=1 ,所以沒(méi)有問(wèn)題,此時(shí)線(xiàn)程2執(zhí)行,線(xiàn)程2 最后也需要加上條件version =1 ,但是現(xiàn)在由于線(xiàn)程1已經(jīng)操作過(guò)了,所以線(xiàn)程2,操作時(shí)就不滿(mǎn)足version=1 的條件了,所以線(xiàn)程2無(wú)法執(zhí)行成功

? 樂(lè)觀(guān)鎖解決庫(kù)存超賣(mài)
加入以下代碼解決超賣(mài)問(wèn)題
之前的方式要修改前后都保持一致,但是這樣我們分析過(guò),成功的概率太低,所以我們的樂(lè)觀(guān)鎖需要變一下,改成stock大于0 即可
boolean success = seckillVoucherService.update()
.setSql("stock= stock -1")
.eq("voucher_id", voucherId).update().gt("stock",0); //where id = ? and stock > 0
知識(shí)拓展
針對(duì)CAS中的自旋壓力過(guò)大,我們可以使用Longaddr這個(gè)類(lèi)去解決
Java8 提供的一個(gè)對(duì)AtomicLong改進(jìn)后的一個(gè)類(lèi),LongAdder
大量線(xiàn)程并發(fā)更新一個(gè)原子性的時(shí)候,天然的問(wèn)題就是自旋,會(huì)導(dǎo)致并發(fā)性問(wèn)題,當(dāng)然這也比我們直接使用syn來(lái)的好
所以利用這么一個(gè)類(lèi),LongAdder來(lái)進(jìn)行優(yōu)化
如果獲取某個(gè)值,則會(huì)對(duì)cell和base的值進(jìn)行遞增,最后返回一個(gè)完整的值

以上的解決方式,依然有些問(wèn)題,下面使用Jmeter進(jìn)行測(cè)試
?Jmeter 測(cè)試
添加線(xiàn)程組

添加JSON斷言,我們認(rèn)為返回結(jié)果為false的就是請(qǐng)求失敗
在線(xiàn)程組右擊選擇斷言 --> JSON 斷言

加入以下判斷

判斷success字段,值是否為true,是true就是返回成功~ 反之失敗
查看結(jié)果樹(shù)、HTTP信息請(qǐng)求頭、匯總報(bào)告、聚合報(bào)告等均在http請(qǐng)求右擊添加即可
啟動(dòng),查看返回的結(jié)果

查看聚合報(bào)告

異常率這么高,再來(lái)看數(shù)據(jù)庫(kù)

數(shù)量正確,我們?cè)倏从唵伪?/p>

id都一樣,這可不行啊,我們真實(shí)場(chǎng)景下,發(fā)放優(yōu)惠卷不會(huì)讓一個(gè)用戶(hù)去搶購(gòu)所有的訂單秒殺優(yōu)惠卷,這樣商家就太虧了,全讓黃牛給搶走了,這可不行,我們需要限制用戶(hù)的搶購(gòu)數(shù)量。
五、優(yōu)惠卷秒殺 實(shí)現(xiàn)一人一單
初步實(shí)現(xiàn)
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用戶(hù)已經(jīng)購(gòu)買(mǎi)過(guò)!");
}
存在問(wèn)題:現(xiàn)在的問(wèn)題還是和之前一樣,并發(fā)過(guò)來(lái),查詢(xún)數(shù)據(jù)庫(kù),都不存在訂單,所以我們還是需要加鎖,但是樂(lè)觀(guān)鎖比較適合更新數(shù)據(jù),而現(xiàn)在是插入數(shù)據(jù),所以我們需要使用悲觀(guān)鎖操作
注意:在這里提到了非常多的問(wèn)題,我們需要慢慢的來(lái)思考,首先我們的初始方案是封裝了一個(gè)createVoucherOrder方法,同時(shí)為了確保他線(xiàn)程安全,在方法上添加了一把synchronized 鎖
加上悲觀(guān)鎖
@Override
public Result seckillVoucher(Long voucherId) {
//1. 查詢(xún)優(yōu)惠卷
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//2. 判斷秒殺是否開(kāi)始 開(kāi)始時(shí)間大于當(dāng)前時(shí)間表示未開(kāi)始搶購(gòu)
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒殺尚未開(kāi)始!");
}
//3. 判斷秒殺是否結(jié)束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒殺已經(jīng)結(jié)束!");
}
//4. 判斷庫(kù)存是否充足
if (seckillVoucher.getStock() < 1) {
return Result.fail("庫(kù)存不足!");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId, userId);
}
}
@Transactional
@Override
public Result createVoucherOrder(Long voucherId, Long userId) {
//5. 查詢(xún)訂單
//5.1 查詢(xún)訂單
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
//5.2 判斷并返回
if (count > 0) {
return Result.fail("用戶(hù)已經(jīng)購(gòu)買(mǎi)過(guò)!");
}
//6. 扣減庫(kù)存
boolean success = seckillVoucherService.update().setSql("stock = stock -1")
.eq("voucher_id", voucherId).gt("stock", 0).
update();
if (!success) {
return Result.fail("庫(kù)存不足!");
}
//7. 創(chuàng)建訂單
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
//8. 返回訂單id
return Result.ok(orderId);
}
在啟動(dòng)類(lèi)加入以下注解,啟動(dòng)AspectJ
@EnableAspectJAutoProxy(exposeProxy = true)
以上代碼,采用悲觀(guān)鎖解決了高并發(fā)下,一人多單的場(chǎng)景,同時(shí),也解決了事務(wù)失效。引入了AspectJ解決!
Jmeter 測(cè)試
再次測(cè)試,查看結(jié)果

可見(jiàn)返回的結(jié)果異常率如此高,再看請(qǐng)求信息

可見(jiàn)已經(jīng)成功的攔截了錯(cuò)誤請(qǐng)求,JSON斷言正確。
查看數(shù)據(jù)庫(kù) 信息

優(yōu)惠卷數(shù)量

可見(jiàn)成功的完成了 在高并發(fā)請(qǐng)求下 的一人一單功能。
?小結(jié)
以上就是【Bug 終結(jié)者】對(duì) 微服務(wù)Spring Boot 整合Redis 實(shí)現(xiàn)優(yōu)惠卷秒殺 一人一單 的簡(jiǎn)單介紹,在分布式系統(tǒng)下,高并發(fā)的場(chǎng)景下,會(huì)出現(xiàn)此類(lèi)庫(kù)存超賣(mài)問(wèn)題,本篇文章介紹了采用樂(lè)觀(guān)鎖來(lái)解決,但是依然是有弊端,下章節(jié),我們將繼續(xù)進(jìn)行優(yōu)化,持續(xù)關(guān)注!
到此這篇關(guān)于Spring Boot 整合Redis 實(shí)現(xiàn)優(yōu)惠卷秒殺 一人一單的文章就介紹到這了,更多相關(guān)Spring Boot 整合Redis 優(yōu)惠卷秒殺內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 微服務(wù)Spring?Boot?整合Redis?阻塞隊(duì)列實(shí)現(xiàn)異步秒殺下單思路詳解
- SpringBoot整合redis及mongodb的詳細(xì)過(guò)程
- springboot整合使用云服務(wù)器上的Redis方法
- SpringBoot整合Redis實(shí)現(xiàn)常用功能超詳細(xì)過(guò)程
- Spring?boot?整合?Redisson實(shí)現(xiàn)分布式鎖并驗(yàn)證功能
- SpringBoot整合Shiro和Redis的示例代碼
- 微服務(wù)Spring Boot 整合 Redis 實(shí)現(xiàn)好友關(guān)注功能
相關(guān)文章
利用Supervisor管理Redis進(jìn)程的方法教程
Supervisor 是可以在類(lèi) UNIX 系統(tǒng)中進(jìn)行管理和監(jiān)控各種進(jìn)程的小型系統(tǒng)。它自帶了客戶(hù)端和服務(wù)端工具,下面這篇文章主要給大家介紹了關(guān)于利用Supervisor管理Redis進(jìn)程的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-08-08
Redis之常用數(shù)據(jù)結(jié)構(gòu)哈希表
這篇文章主要介紹了Redis常用的數(shù)據(jù)結(jié)構(gòu)哈希表,哈希表是一種保存鍵值對(duì)的數(shù)據(jù)結(jié)構(gòu),具有一定的參考價(jià)值,需要的朋友可以參考閱讀2023-04-04
Redis集群指定主從關(guān)系及動(dòng)態(tài)增刪節(jié)點(diǎn)方式
這篇文章主要介紹了Redis集群指定主從關(guān)系及動(dòng)態(tài)增刪節(jié)點(diǎn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
Redis高并發(fā)場(chǎng)景下秒殺超賣(mài)解決方案(秒殺場(chǎng)景)
早起的12306購(gòu)票,剛被開(kāi)發(fā)出來(lái)使用的時(shí)候,12306會(huì)經(jīng)常出現(xiàn)超賣(mài) 這種現(xiàn)象,也就是說(shuō)車(chē)票只剩10張了,卻被20個(gè)人買(mǎi)到了,這種現(xiàn)象就是超賣(mài),今天通過(guò)本文給大家介紹Redis高并發(fā)場(chǎng)景下秒殺超賣(mài)解決方案,感興趣的朋友一起看看吧2022-04-04

