在上一篇博客中,我們介紹了如何利用 Redis 和 Lua 腳本來高效處理秒殺活動中的高并發請求,保證用戶體驗。本文將進一步優化秒殺系統,通過引入阻塞隊列實現異步下單,從而提高系統的整體性能和穩定性。
引言
秒殺活動往往伴隨著極高的并發請求,對系統的性能和穩定性提出了巨大挑戰。同步處理訂單請求可能導致數據庫壓力過大,影響系統響應時間。為了緩解這一問題,我們可以采用異步下單的方式,將訂單請求先放入阻塞隊列,由后臺線程逐一處理,從而降低數據庫的瞬時壓力。
方案設計
基本思路
- 用戶發起秒殺請求,先通過 Redis Lua 腳本進行資格判斷。
- 通過 Lua 腳本判斷用戶是否有購買資格,并扣減庫存。
- 將訂單信息放入阻塞隊列中,由后臺線程異步處理訂單創建和數據庫操作。
- 返回訂單 ID 給用戶。
具體實現
Lua 腳本
Lua 腳本的邏輯保持不變,繼續用于判斷秒殺資格和扣減庫存。
Java 代碼
在 Java 代碼中,我們通過阻塞隊列實現異步下單,并利用 Redisson 分布式鎖來確保訂單操作的線程安全。
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedissonClient redissonClient;private static final DefaultRedisScript<Long> SECKILL_SCRIPT;static {SECKILL_SCRIPT = new DefaultRedisScript<>();SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));SECKILL_SCRIPT.setResultType(Long.class);}private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();@PostConstructprivate void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());}private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while (true) {try {VoucherOrder voucherOrder = orderTasks.take();handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("創建訂單失敗", e);}}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();RLock lock = redissonClient.getLock("lock:order:" + userId);boolean isLock = lock.tryLock();if (!isLock) {log.error("不允許重復下單");return;}try {proxy.crateVoucherOrder(voucherOrder);} finally {lock.unlock();}}private IVoucherOrderService proxy;@Overridepublic Result seckillVoucher(Long voucherId) {Long userId = UserHolder.getUser().getId();Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString());int r = result.intValue();if (r != 0) {return Result.fail(r == 1 ? "庫存不足" : "不能重復下單");}VoucherOrder voucherOrder = new VoucherOrder();long orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);voucherOrder.setUserId(userId);voucherOrder.setVoucherId(voucherId);orderTasks.add(voucherOrder);proxy = (IVoucherOrderService) AopContext.currentProxy();return Result.ok(orderId);}@Override@Transactionalpublic void crateVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();if (count > 0) {log.error("不允許重復下單!");return;}boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id",voucherOrder.getVoucherId()).gt("stock", 0).update();if (!success) {log.error("庫存不足!");return;}save(voucherOrder);}
}
代碼詳解
-
初始化阻塞隊列和線程池:
- 使用
BlockingQueue
和ExecutorService
實現一個單線程的訂單處理機制,在服務初始化時啟動訂單處理線程。
- 使用
-
秒殺請求處理:
- 用戶發起秒殺請求時,首先通過 Lua 腳本判斷秒殺資格和扣減庫存。
- 如果有購買資格,將訂單信息放入阻塞隊列中。
-
訂單處理線程:
- 訂單處理線程從阻塞隊列中取出訂單,并在獲取到用戶鎖后創建訂單,防止同一用戶重復下單。
-
事務處理:
- 在訂單處理方法中使用事務管理,確保訂單創建和庫存扣減的原子性。
結論
通過引入阻塞隊列實現異步下單,我們有效地減少了數據庫的瞬時壓力,提高了系統的整體性能和穩定性。這種方法不僅適用于秒殺活動,還可以推廣到其他高并發場景,如搶購、促銷活動等。希望本文對您理解和實現高并發系統有所幫助。
可能出現的問題
我在一次批量用一千個線程去搶優惠卷的時候發現,優惠卷沒有搶完,初步判斷是阻塞隊列的大小過小,內存的限制問題。