秒殺最佳化-基於阻塞佇列實現秒殺最佳化

捞月亮的小北發表於2024-07-27

秒殺最佳化

VoucherOrderServiceImpl

修改下單動作,現在我們去下單時,是透過lua表示式去原子執行判斷邏輯,如果判斷我出來不為0 ,則要麼是庫存不足,要麼是重複下單,返回錯誤資訊,如果是0,則把下單的邏輯儲存到佇列中去,然後非同步執行

@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private 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();

    private IVoucherOrderService proxy;

    // 在類初始化之後執行,因為當這個類初始化好了之後,隨時都是有可能要執行的
    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    // 用於執行緒池處理的任務
    // 當初始化完畢後,就會去從對列中去拿資訊
    private class VoucherOrderHandler implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    // 1.獲取佇列中的訂單資訊
                    VoucherOrder voucherOrder = orderTasks.take();
                    // 2.建立訂單
                    handleVoucherOrder(voucherOrder);
                } catch (Exception e) {
                    log.error("處理訂單異常", e);
                }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        // 1. 獲取使用者
        Long userId = voucherOrder.getUserId();

        // 2. 建立鎖物件
        RLock redisLock = redissonClient.getLock("lock:order:" + userId);

        // 3. 嘗試獲取鎖
        boolean isLock = false;
        try {
            // 嘗試獲取鎖,設定等待時間和鎖自動釋放時間
            // 如果鎖不可用,則等待 1 秒鐘;如果鎖可用,則獲取鎖並設定鎖自動釋放時間為 10 秒
            isLock = redisLock.tryLock(1, 10, TimeUnit.SECONDS);

            // 4. 判斷是否獲得鎖成功
            if (!isLock) {
                // 獲取鎖失敗,直接返回失敗或者重試
                log.error("不允許重複下單!");
                return;
            }
            // 注意:由於 Spring 的事務管理是放在 ThreadLocal 中,此時是多執行緒環境,事務可能會失效
            proxy.createVoucherOrder(voucherOrder);
        } catch (InterruptedException e) {
            // 如果執行緒被中斷,處理中斷異常
            Thread.currentThread().interrupt();
            log.error("執行緒被中斷", e);
        } finally {
            // 釋放鎖
            if (isLock) { // 只有當成功獲取鎖時才釋放鎖
                redisLock.unlock();
            }
        }
    }

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 獲取使用者
        Long userId = UserHolder.getUser().getId();
        // 1.執行lua指令碼
        Long result = stringRedisTemplate.execute(
                SECKILL_SCRIPT,
                Collections.emptyList(),
                voucherId.toString(), userId.toString()
        );
        int r = result.intValue();
        // 2.判斷結果是否為0
        if (r != 0) {
            // 2.1.不為0 ,代表沒有購買資格
            return Result.fail(r == 1 ? "庫存不足" : "不能重複下單");
        }
        VoucherOrder voucherOrder = new VoucherOrder();
        // 2.3.訂單id
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 2.4.使用者id
        voucherOrder.setUserId(userId);
        // 2.5.代金券id
        voucherOrder.setVoucherId(voucherId);
        // 2.6.放入阻塞佇列
        orderTasks.add(voucherOrder);
        // 3.獲取代理物件
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        // 3.返回訂單id
        return Result.ok(orderId);
    }

/*     @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查詢優惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判斷秒殺是否開始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未開始
            return Result.fail("秒殺尚未開始!");
        }
        // 3.判斷秒殺是否已經結束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未開始
            return Result.fail("秒殺已經結束!");
        }
        // 4.判斷庫存是否充足
        if (voucher.getStock() < 1) {
            // 庫存不足
            return Result.fail("庫存不足!");
        }

        Long userId = UserHolder.getUser().getId();

        // synchronized (userId.toString().intern()) {

        // 嘗試建立鎖物件
        // SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("order:" + userId);
        // 獲取鎖
        boolean isLock = lock.tryLock();
        // boolean isLock = lock.tryLock();

        if (!isLock) {
            Result.fail("不允許重複下單");
        }
        // 獲取代理物件 (事務)
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }
        // }

    } */

    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        // 5.1.查詢訂單
        int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
        // 5.2.判斷是否存在
        if (count > 0) {
            // 使用者已經購買過了
            log.error("使用者已經購買過了");
            return;
        }

        // 6.扣減庫存
        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("庫存不足");
            return;
        }
        save(voucherOrder);

    }
}

小總結:

秒殺業務的最佳化思路是什麼?

  • 先利用Redis完成庫存餘量、一人一單判斷,完成搶單業務
  • 再將下單業務放入阻塞佇列,利用獨立執行緒非同步下單
  • 基於阻塞佇列的非同步秒殺存在哪些問題?
    • 記憶體限制問題
    • 資料安全問題

相關文章