秒殺最佳化
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完成庫存餘量、一人一單判斷,完成搶單業務
- 再將下單業務放入阻塞佇列,利用獨立執行緒非同步下單
- 基於阻塞佇列的非同步秒殺存在哪些問題?
- 記憶體限制問題
- 資料安全問題