優惠券秒殺-一人一單
需求:修改秒殺業務,要求同一個優惠券,一個使用者只能下一單
優惠卷是為了引流,但是目前的情況是,一個人可以無限制的搶這個優惠卷,所以我們應當增加一層邏輯,讓一個使用者只能下一個單,而不是讓一個使用者下多個單
具體操作邏輯如下:比如時間是否充足,如果時間充足,則進一步判斷庫存是否足夠,然後再根據優惠卷id和使用者id查詢是否已經下過這個訂單,如果下過這個訂單,則不再下單,否則進行下單
@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("庫存不足!"); } // 5.一人一單邏輯 // 5.1.使用者id Long userId = UserHolder.getUser().getId(); int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); // 5.2.判斷是否存在 if (count > 0) { // 使用者已經購買過了 return Result.fail("使用者已經購買過一次!"); }//6,扣減庫存 boolean success = seckillVoucherService.update() .setSql("stock= stock -1") .eq("voucher_id", voucherId).update(); if (!success) { //扣減庫存 return Result.fail("庫存不足!"); } //7.建立訂單 VoucherOrder voucherOrder = new VoucherOrder(); // 7.1.訂單id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); return Result.ok(orderId); }
這裡有什麼問題呢?
我們必須要保證 判斷庫存是否充足,判斷訂單是否存在,扣減庫存,建立訂單 這一系列操作是原子性的。我在做這件事的時候,別人不能加進來。
怎麼保證呢,還是加鎖。
@Transactional public synchronized Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId(); // 5.1.查詢訂單 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); // 5.2.判斷是否存在 if (count > 0) { // 使用者已經購買過了 return Result.fail("使用者已經購買過一次!"); } // 6.扣減庫存 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") // set stock = stock - 1 .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0 .update(); if (!success) { // 扣減失敗 return Result.fail("庫存不足!"); } // 7.建立訂單 VoucherOrder voucherOrder = new VoucherOrder(); // 7.1.訂單id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); // 7.2.使用者id voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); // 7.返回訂單id return Result.ok(orderId); }
Long userId = UserHolder.getUser().getId();
當我們使用了userId.toString()時,我們建立的鎖物件是通一個嗎? 點進去toString()發現並不是同一個,最終是new了一物件。 這時候需要用到我們的intern()方法
intern() 這個方法是從常量池中拿到資料,如果我們直接使用userId.toString() 他拿到的物件實際上是不同的物件,new出來的物件,我們使用鎖必須保證鎖必須是同一把,所以我們需要使用intern()方法.
@Transactional public Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId(); synchronized(userId.toString().intern()){ // 5.1.查詢訂單 int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); // 5.2.判斷是否存在 if (count > 0) { // 使用者已經購買過了 return Result.fail("使用者已經購買過一次!"); } // 6.扣減庫存 boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") // set stock = stock - 1 .eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0 .update(); if (!success) { // 扣減失敗 return Result.fail("庫存不足!"); } // 7.建立訂單 VoucherOrder voucherOrder = new VoucherOrder(); // 7.1.訂單id long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); // 7.2.使用者id voucherOrder.setUserId(userId); // 7.3.代金券id voucherOrder.setVoucherId(voucherId); save(voucherOrder); // 7.返回訂單id return Result.ok(orderId); } }
現在還有什麼問題?
我們鎖在執行完業務邏輯後,釋放鎖了鎖,但是spring的事務需要在方法執行完成才會去提交程式碼,那麼這中間仍然可能會出問題。
在seckillVoucher 方法中,新增以下邏輯,這樣就能保證事務的特性,同時也控制了鎖的粒度。
Long id = UserHolder.getUser().getId(); synchronized (id.toString().intern()){ return createVoucherOrder(voucherId); }
這樣看似解決了我們的問題,實際上spring控制的事務卻失效了。 spring事務和aop本質上是依據spring的代理物件完成控制的,我們在本方法使用的是省卻this物件,沒有使用代理物件,所以事務不能生效。
我們要怎麼做呢, 使用代理物件去呼叫,怎麼獲取代理物件?
Long id = UserHolder.getUser().getId(); synchronized (id.toString().intern()){ IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy(); return proxy.createVoucherOrder(voucherId); }
獲取AopContext的當前代理物件,那麼這個代理物件是Object型別,當前是在 IVoucherOrderService 的實現類中,那麼我們當前的代理物件就是 IVoucherOrderService 。 使用這種獲取代理物件的時候還需要aspectj的依賴和
啟動類上的註解去暴露這個代理物件,不暴露我們是獲取不到的。
至此單體架構的一人一單功能已經實現。
隨著業務的擴充套件,我們使用了叢集部署,多臺tomcat 同時進行,這個方法還能生效嗎?