優化邏輯
把耗時較短的邏輯判斷放入redsi中,比如庫存是否足夠以及是否一人一單,只要這樣的邏輯完成,就代表一定能下單成功,我們就將結果返回給用戶,然后我們再開一個線程慢慢執行隊列中的信息
問題:
如何快速校驗一人一單以及庫存是否充足
交驗和下單是兩個線程,如何將二者對應:
在redis操作完成之后,會返回一些信息給前端,同時將這些信息丟給異步隊列執行,后續操作通過id來查詢下單邏輯是否完成
整體流程
下單后判斷是否充足只需要去redis根據key查詢對應的value是否大于0 ,如果大于0再判斷是否下過單,如果在set集合中沒有這條數據,那么就將userId和優惠卷存入redis,將優惠卷id、用戶id和訂單id存入阻塞隊列中,異步存儲到數據庫
stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
保存優惠卷并將保存秒殺的庫存到Redis
-- 3.腳本業務
-- 3.1.判斷庫存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then-- 3.2.庫存不足,返回1return 1
end
-- 3.2.判斷用戶是否下單 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,說明是重復下單,返回2return 2
end
-- 3.6.發送消息到隊列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
只要>0就可以下單,然后判斷用戶是否下過單
命令結構:
XADD stream.orders * k1 v1 k2 v2 ...
- stream.orders
:目標流的名稱。
- *
:自動生成唯一的消息 ID(格式為 時間戳-序列號
)。
- k1 v1 k2 v2 ...
:消息的鍵值對數據。
Long result = stringRedisTemplate.execute( SECKILL_SCRIPT, Collections.emptyList(), voucherId.toString(), userId.toString(), String.valueOf(orderId)
);
腳本參數:
- SECKILL_SCRIPT
:預定義的 Lua 腳本,處理秒殺業務邏輯(如庫存校驗、扣減)。
- Collections.emptyList()
:Lua 腳本中KEYS
參數為空列表(無需鍵名參數)。
- 后續參數為ARGV
數組,依次是voucherId
、userId
、orderId
,供 Lua 腳本內部使用。
private static final ExecutorService SECKILL_ORDER_EXECUTOR=Executors.newSingleThreadExecutor();
定義了一個靜態常量線程池,這是一個單線程的執行器,保證任務按順序執行, 避免多線程并發處理同一用戶訂單導致的重復下單問題
// 類初始化后啟動工作線程
@PostConstruct
private void init() { SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); }
@PostConstruct確保類初始化后立即啟動訂單處理線程,并會持續運行直到應用關閉
private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {while (true){try {// 1.獲取隊列中的訂單信息VoucherOrder voucherOrder = orderTasks.take();// 2.創建訂單handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("處理訂單異常", e);}}}}
實現 Runnable
接口的類通常用于創建線程任務,可以通過 Thread
類或線程池執行。
orderTasks.take()
是阻塞調用,隊列空時線程會等待- 確保訂單按放入隊列的順序處理
- 異常處理保證線程不會因異常終止
proxy.createVoucherOrder(voucherOrder);
- Spring 事務依賴 ThreadLocal,多線程環境下子線程無法獲取主線程的事務上下文
- 通過注入代理對象
proxy
調用事務方法,確保事務生效
也就是說繼承Runnable接口的類可以被在線程池中被調用
而這里選擇了在類初始化之后就調用
Spring 事務管理的工作原理
private void handleVoucherOrder(VoucherOrder voucherOrder) {// ...獲取鎖...try {// 通過代理對象調用事務方法proxy.createVoucherOrder(voucherOrder);} finally {// ...釋放鎖...}
}
Spring 的聲明式事務是通過 AOP 代理實現的。當在方法上使用@Transactional
注解時,Spring 會為該類創建一個代理對象,在調用帶注解的方法時,代理會攔截調用并添加事務管理邏輯。
如果直接使用this.createVoucherOrder(voucherOrder),事務不會生效,因為AOP代理被繞過了,必須通過代理對象調用這個方法才能觸發事務增強
因為:
- Spring 事務是基于
ThreadLocal
實現的,不同線程有獨立的ThreadLocal
副本 - 子線程(如線程池中的工作線程)無法獲取主線程的事務上下文
- 必須通過代理對象調用才能確保事務攔截器被觸發
使用AopContext.currentProxy()
:在方法內部獲取當前代理對象
總結
1. 為什么將庫存校驗和一人一單判斷放在 Redis 中執行?
將高頻、低耗時的校驗邏輯放在 Redis 中執行,利用其內存級別的讀寫性能和原子性操作能力,可以快速完成資格校驗。同時避免了直接訪問數據庫帶來的網絡延遲和 IO 開銷,顯著提升系統吞吐量。
2. 如何保證庫存扣減和訂單記錄的原子性?
通過 Lua 腳本在 Redis 端實現原子操作。腳本中先校驗庫存和用戶下單狀態,若滿足條件則直接扣減庫存并記錄訂單信息,整個過程不可分割,有效防止超賣和重復下單問題。
3. 異步處理訂單時,如何保證數據最終一致性?
采用消息隊列實現異步解耦,主流程完成 Redis 操作后立即返回結果,同時將訂單信息發送到阻塞隊列。獨立線程按順序處理隊列中的訂單,確保數據最終一致性。即使處理過程中出現異常,也可通過重試機制保證訂單最終入庫。
4. 為什么使用單線程執行器處理訂單隊列?
使用單線程執行器(Executors.newSingleThreadExecutor()
)可以確保同一用戶的訂單按順序處理,避免多線程并發處理導致的重復下單問題。同時保證了操作的順序性,與 Redis 中的校驗邏輯形成完整閉環。
5. 在多線程環境下,如何保證 Spring 事務生效?
在子線程中通過注入代理對象調用事務方法,而非直接使用this
引用。因為 Spring 事務基于 AOP 代理和ThreadLocal
實現,子線程無法直接獲取主線程的事務上下文。通過代理對象調用可確保事務攔截器被觸發,從而正確管理事務。
6. Redis 消息隊列相比傳統阻塞隊列有什么優勢?
Redis 的 Stream 數據結構支持持久化和多消費者組,相比 Java 內置的阻塞隊列,具有更好的可靠性和擴展性。即使服務重啟,未處理的消息也不會丟失,適合分布式系統下的異步通信場景。