Redis實(shí)戰(zhàn)之Redis實(shí)現(xiàn)異步秒殺優(yōu)化詳解
秒殺優(yōu)化-異步秒殺思路
未優(yōu)化的思路
當(dāng)用戶(hù)發(fā)起請(qǐng)求,此時(shí)會(huì)請(qǐng)求nginx,nginx會(huì)訪(fǎng)問(wèn)到tomcat,而tomcat中的程序,會(huì)進(jìn)行串行操作,分成如下幾個(gè)步驟
1、查詢(xún)優(yōu)惠卷
2、判斷秒殺庫(kù)存是否足夠
3、查詢(xún)訂單
4、校驗(yàn)是否是一人一單
5、扣減庫(kù)存
6、創(chuàng)建訂單
在這六步操作中,又有很多操作是要去操作數(shù)據(jù)庫(kù)的,而且還是一個(gè)線(xiàn)程串行執(zhí)行, 這樣就會(huì)導(dǎo)致我們的程序執(zhí)行的很慢

優(yōu)化方案
我們將耗時(shí)比較短的邏輯判斷放入到redis中,比如是否庫(kù)存足夠,比如是否一人一單,這樣的操作,只要這種邏輯可以完成,就意味著我們是一定可以下單完成的,我們只需要進(jìn)行快速的邏輯判斷,根本就不用等下單邏輯走完,我們直接給用戶(hù)返回成功, 再在后臺(tái)開(kāi)一個(gè)線(xiàn)程,后臺(tái)線(xiàn)程慢慢的去執(zhí)行queue里邊的消息,即不追求時(shí)效性,讓用戶(hù)先成功下單,后續(xù)再完善數(shù)據(jù)庫(kù)數(shù)據(jù)

整體思路
用戶(hù)下單之后,判斷庫(kù)存是否充足只需要到redis中去根據(jù)key找對(duì)應(yīng)的value是否大于0即可,如果不充足,則直接結(jié)束,如果充足,繼續(xù)在redis中判斷用戶(hù)是否可以下單,如果set集合中沒(méi)有這條數(shù)據(jù),說(shuō)明他可以下單,如果set集合中沒(méi)有這條記錄,則將userId和優(yōu)惠卷存入到redis中,并且返回0,整個(gè)過(guò)程需要保證是原子性的,我們可以使用lua來(lái)操作
當(dāng)以上判斷邏輯走完之后,我們可以判斷當(dāng)前redis中返回的結(jié)果是否是0 ,如果是0,則表示可以下單,則將之前說(shuō)的信息存入到到queue中去,然后返回,然后再來(lái)個(gè)線(xiàn)程異步的下單,前端可以通過(guò)返回的訂單id來(lái)判斷是否下單成功。

難點(diǎn)
- 怎么在redis中去快速校驗(yàn)一人一單,還有庫(kù)存判斷
- 由于我們校驗(yàn)和tomct下單是兩個(gè)線(xiàn)程,那么我們?nèi)绾沃赖降啄膫€(gè)單他最后是否成功,或者是下單完成,為了完成這件事我們?cè)趓edis操作完之后,我們會(huì)將一些信息返回給前端,同時(shí)也會(huì)把這些信息丟到異步queue中去,后續(xù)操作中,可以通過(guò)這個(gè)id來(lái)查詢(xún)我們tomcat中的下單邏輯是否完成了。
代碼實(shí)現(xiàn)
需求:
- 新增秒殺優(yōu)惠券的同時(shí),將優(yōu)惠券信息,優(yōu)惠券id和庫(kù)存信息保存到Redis中
- 基于Lua腳本,判斷秒殺庫(kù)存、一人一單,決定用戶(hù)是否搶購(gòu)成功
- 如果搶購(gòu)成功,將優(yōu)惠券id和用戶(hù)id封裝后存入阻塞隊(duì)列
- 開(kāi)啟線(xiàn)程任務(wù),不斷從阻塞隊(duì)列中獲取信息,實(shí)現(xiàn)異步下單功能
新增優(yōu)惠券,將優(yōu)惠券信息入庫(kù)并寫(xiě)入redis
@Override
@Transactional
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);
//存入redis
stringRedisTemplate.opsForValue().setIfAbsent(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
}判斷秒殺庫(kù)存、一人一單,決定用戶(hù)是否搶購(gòu)成功,考慮到操作的原子性,采用lua腳本完成這一連串的操作
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by Lenovo.
--- DateTime: 2023/9/5 20:57
---
-- 1.參數(shù)列表
-- 1.1.優(yōu)惠券id
local voucherId = ARGV[1]
-- 1.2.用戶(hù)id
local userId = ARGV[2]
---- 1.3.訂單id
local orderId = ARGV[3]
-- 2.數(shù)據(jù)key
-- 2.1.庫(kù)存key
local stockKey = 'seckill:stock:' .. voucherId
---- 2.2.訂單key
local orderKey = 'seckill:order:' .. voucherId
-- 3.腳本業(yè)務(wù)
-- 3.1.判斷庫(kù)存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.庫(kù)存不足,返回1
return 1
end
-- 3.2.判斷用戶(hù)是否下單 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,說(shuō)明是重復(fù)下單,返回2
return 2
end
-- 3.4.扣庫(kù)存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下單(保存用戶(hù))sadd orderKey userId
redis.call('sadd', orderKey, userId)
---- 3.6.發(fā)送消息到隊(duì)列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0執(zhí)行l(wèi)ua腳本,判斷是否搶購(gòu)成功,如果搶購(gòu)成功,要放入堵塞隊(duì)列中
@Override
public Result seckillVoucher(Long voucherId) {
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//判斷是否開(kāi)始,開(kāi)始時(shí)間如果在當(dāng)前時(shí)間之后就是尚未開(kāi)始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒殺尚未開(kāi)始");
}
//判斷是否結(jié)束,結(jié)束時(shí)間如果在當(dāng)前時(shí)間之前就是已經(jīng)結(jié)束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒殺已經(jīng)結(jié)束");
}
Long userId = UserHolder.getUser().getId();
long orderId = new RedisIdWorker(stringRedisTemplate).nextId("order");
Long execute = stringRedisTemplate.execute(SILLL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString(), String.valueOf(orderId)
);
int r = execute.intValue();
if (r != 0) {
return Result.fail(r == 1 ? "庫(kù)存不足" : "不能重復(fù)下單");
}
VoucherOrder voucherOrder = new VoucherOrder();
//訂單id
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
voucherOrder.setId(orderId);
//將訂單信息放入阻塞隊(duì)列
orderTakes.add(voucherOrder);
return Result.ok(orderId);
}定義線(xiàn)程內(nèi)部類(lèi),不斷從堵塞隊(duì)列中讀取訂單
//從阻塞隊(duì)列里面取訂單信息
private class voucherOrderHander implements Runnable {
@Override
public void run() {
while (true) {
try {
VoucherOrder take = orderTakes.take();
handleVoucherOrder(take);
} catch (Exception e) {
log.error("異常信息如下", e);
}
}
}獲取訂單信息的具體方法,這里依然加了分布式鎖,是為了保險(xiǎn)起見(jiàn)
private void handleVoucherOrder(VoucherOrder take) {
Long userId = take.getId();
//創(chuàng)建鎖對(duì)象
RLock lock = redissonClient.getLock("lock:order:" + userId);
//嘗試獲取鎖
boolean isLock = lock.tryLock();
//獲取鎖失敗
if (!isLock) {
log.error("不允許重復(fù)下單");
return;
}
try {
voucherOrderService.createVoucherOrder(take);
} finally {
//釋放鎖
lock.unlock();
}
}
}這里又有一個(gè)問(wèn)題,就是我們訂單信息入庫(kù)應(yīng)該是在該類(lèi)對(duì)象被創(chuàng)建的時(shí)候就要開(kāi)啟線(xiàn)程在堵塞隊(duì)列等待讀取是否有訂單信息,然后順利入庫(kù),所以我們用了aop的@PostConstruct,保證該對(duì)象被創(chuàng)建時(shí),線(xiàn)程也能順利創(chuàng)建,這里用了線(xiàn)程池來(lái)提交線(xiàn)程任務(wù)
@PostConstruct
public void init() {
SECKILL_ORDER_EXECUTOR.execute(new voucherOrderHander());
}完整代碼實(shí)現(xiàn)
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private ISeckillVoucherService seckillVoucherService;
@Autowired
private RedisIdWorker redisIdWorker;
@Autowired
private IVoucherOrderService voucherOrderService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
private static final DefaultRedisScript<Long> SILLL_SCRIPT;
BlockingQueue<VoucherOrder> orderTakes = new ArrayBlockingQueue<>(1024 * 1024);
//異步處理線(xiàn)程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
static {
SILLL_SCRIPT = new DefaultRedisScript<>();
SILLL_SCRIPT.setLocation(new ClassPathResource("skill.lua"));
SILLL_SCRIPT.setResultType(Long.class);
}
@PostConstruct
public void init() {
SECKILL_ORDER_EXECUTOR.execute(new voucherOrderHander());
}
//從阻塞隊(duì)列里面取用戶(hù)信息
private class voucherOrderHander implements Runnable {
@Override
public void run() {
while (true) {
try {
VoucherOrder take = orderTakes.take();
handleVoucherOrder(take);
} catch (Exception e) {
log.error("異常信息如下", e);
}
}
}
private void handleVoucherOrder(VoucherOrder take) {
Long userId = take.getId();
//創(chuàng)建鎖對(duì)象
RLock lock = redissonClient.getLock("lock:order:" + userId);
//嘗試獲取鎖
boolean isLock = lock.tryLock();
//獲取鎖失敗
if (!isLock) {
log.error("不允許重復(fù)下單");
return;
}
try {
voucherOrderService.createVoucherOrder(take);
} finally {
//釋放鎖
lock.unlock();
}
}
}
@Override
public Result seckillVoucher(Long voucherId) {
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//判斷是否開(kāi)始,開(kāi)始時(shí)間如果在當(dāng)前時(shí)間之后就是尚未開(kāi)始
if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒殺尚未開(kāi)始");
}
//判斷是否結(jié)束,結(jié)束時(shí)間如果在當(dāng)前時(shí)間之前就是已經(jīng)結(jié)束
if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒殺已經(jīng)結(jié)束");
}
Long userId = UserHolder.getUser().getId();
long orderId = new RedisIdWorker(stringRedisTemplate).nextId("order");
Long execute = stringRedisTemplate.execute(SILLL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString(), String.valueOf(orderId)
);
int r = execute.intValue();
if (r != 0) {
return Result.fail(r == 1 ? "庫(kù)存不足" : "不能重復(fù)下單");
}
VoucherOrder voucherOrder = new VoucherOrder();
//訂單id
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
voucherOrder.setId(orderId);
//將訂單信息放入阻塞隊(duì)列
orderTakes.add(voucherOrder);
return Result.ok(orderId);
}
@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
// 5.1.查詢(xún)訂單
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
// 5.2.判斷是否存在
if (count > 0) {
// 用戶(hù)已經(jīng)購(gòu)買(mǎi)過(guò)了
log.error("用戶(hù)已經(jīng)購(gòu)買(mǎi)過(guò)了");
return;
}
// 6.扣減庫(kù)存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣減失敗
log.error("庫(kù)存不足");
return;
}
save(voucherOrder);
}以上就是Redis實(shí)戰(zhàn)之Redis實(shí)現(xiàn)異步秒殺優(yōu)化詳解的詳細(xì)內(nèi)容,更多關(guān)于Redis實(shí)現(xiàn)異步秒殺優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Quarkus集成redis操作Redisson實(shí)現(xiàn)數(shù)據(jù)互通
這篇文章主要為大家介紹了Quarkus集成redis操作Redisson實(shí)現(xiàn)數(shù)據(jù)互通的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02
Centos7.3安裝Redis4.0.6詳細(xì)圖文教程
這篇文章主要介紹了Centos7.3安裝Redis4.0.6詳細(xì)教程圖解,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-10-10
WINDOWS中REDIS主從配置實(shí)現(xiàn)代碼解析
這篇文章主要介紹了WINDOWS中REDIS主從配置實(shí)現(xiàn)代碼解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
redis緩存與數(shù)據(jù)庫(kù)一致性的問(wèn)題及解決
這篇文章主要介紹了redis緩存與數(shù)據(jù)庫(kù)一致性的問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
Redis中一些最常見(jiàn)的面試問(wèn)題總結(jié)
Redis在互聯(lián)網(wǎng)技術(shù)存儲(chǔ)方面使用如此廣泛,幾乎所有的后端技術(shù)面試官都要在Redis的使用和原理方面對(duì)小伙伴們進(jìn)行各種刁難。下面這篇文章主要給大家總結(jié)介紹了關(guān)于Redis中一些最常見(jiàn)的面試問(wèn)題,需要的朋友可以參考下2018-09-09
記Redis集群掛掉后,嘗試重啟集群但失敗問(wèn)題及解決
Redis集群重建失敗原因解析:數(shù)據(jù)殘留與持久化機(jī)制沖突,需清理AOF/RDB文件及nodes.conf配置文件,確保集群冷啟動(dòng),避免舊數(shù)據(jù)干擾2026-06-06

