功能03-優惠券秒殺02
4.功能03-優惠券秒殺
4.4一人一單
4.4.1需求分析
要求:修改秒殺業務,要求同一個優惠券,一個用戶只能下一單,
在之前的做法中,加入一個對用戶id和優惠券id的判斷,如果在優惠券下單表中已經存在,則表示該用戶對于這張優惠券已經下過單了,不允許重復購買
4.4.2代碼實作
(1)修改VoucherOrderServiceImpl的seckillVoucher方法,在扣減庫存之前,加入如下邏輯:
//一人一單
Long userId = UserHolder.getUser().getId();
//查詢訂單
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {//說明已經該用戶已經對該優惠券下過單了
return Result.fail("用戶已經購買過一次!");
}
(2)使用jemeter進行測驗:由同一個用戶發起200個并發執行緒,進行下單請求
測驗結果:查看資料庫發現,秒殺券原本有100張,現在只剩下94張,也就是說一個用戶搶購了多張同樣的券
(3)原因分析:
因為是多執行緒并發操作,假設當前資料庫中沒有某個用戶的對應券的訂單,這時,有100個執行緒來執行(1)代碼的邏輯,大家都來查詢訂單,都發現該用戶沒有下過訂單,因此都進行之后的下單操作,于是一個用戶就連續插入了多條訂單記錄,根本原因還是執行緒并發的安全問題,
(4)解決方案:使用悲觀鎖,
修改VoucherOrderServiceImpl:
我們將查詢用戶是否購買過某個優惠券的功能,以及扣減庫存、下單功能抽取到一個方法createVoucherOrder()中,在seckillVoucher方法中,通過synchronized鎖定物件(用戶id),這樣同一個用戶發起多個執行緒時,多個執行緒同時只能有一個執行緒進入到createVoucherOrder()中(不同用戶的不同執行緒不受影響),然后去判斷是否符合業務,從而實作一人一單的問題,
package com.hmdp.service.impl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* 服務實作類
*
* @author 李
* @version 1.0
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
public Result seckillVoucher(Long voucherId) {
//根據id查詢優惠券資訊
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher == null) {
return Result.fail("該優惠券不存在,請重繪!");
}
//判斷秒殺券是否在有效時間內
//若不在有效期,則回傳例外結果
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒殺尚未開始!");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒殺已經結束!");
}
//若在有效期,判斷庫存是否充足
if (voucher.getStock() < 1) {//庫存不足
return Result.fail("秒殺券庫存不足!");
}
Long userId = UserHolder.getUser().getId();
//即使是同一個userId,在不同執行緒中呼叫toString得到的是不同的字串物件,synchronized無法鎖定
//因此這里還要使用intern()方法:
//呼叫intern()時,如果常量池中已經包含一個等于這個String物件(由equals(Object)方法確定)的字串,
//則回傳池中的字串,否則將此String物件添加到常量池中并回傳該String物件的參考
//先獲取鎖,然后提交createVoucherOrder()的事務,再釋放鎖,才能確保執行緒是安全的
synchronized (userId.toString().intern()) {
//spring宣告式事務的原理,通過aop的動態代理實作,獲取到這個動態代理,讓動態代理去呼叫方法
IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//一人一單
Long userId = UserHolder.getUser().getId();
//查詢訂單
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {//說明已經該用戶已經對該優惠券下過單了
return Result.fail("用戶已經購買過一次!");
}
//庫存充足,則扣減庫存(操作秒殺券表)
boolean success = seckillVoucherService.update()
.setSql("stock = stock -1")//set stock = stock -1
//where voucher_id =? and stock>0
.gt("stock", 0).eq("voucher_id", voucherId).update();
if (!success) {//操作失敗
return Result.fail("秒殺券庫存不足!");
}
//扣減庫存成功,則創建訂單,回傳訂單id
VoucherOrder voucherOrder = new VoucherOrder();
//設定訂單id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//設定用戶id
//Long userId = UserHolder.getUser().getId();
voucherOrder.setUserId(userId);
//設定代金券id
voucherOrder.setVoucherId(voucherId);
//將訂單寫入資料庫(操作優惠券訂單表)
save(voucherOrder);
return Result.ok(orderId);
}
}
(5)引入依賴
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
(6)主程式中添加注解@EnableAspectJAutoProxy:
(7)IVoucherOrderService中添加方法宣告:
(8)重新進行(2)的測驗,可以看到,同一個用戶對一種優惠券同時發起200個執行緒請求下單,結果是:成功下單,且只能下單一次
4.5分布式鎖
4.5.1問題提出(集群模式下的執行緒并發問題)
通過加鎖,可以解決在單機情況下的一人一單安全問題,但是在集群模式下就不行了:
(1)我們將服務啟動兩份,埠分別為8081,8082:
View--Tool Windows--Services
點擊add service,選擇Run Configuration Type,選擇SpringBoot

按如下步驟配置,然后點擊apply
點擊啟動新的專案,形成一個集群:
(2)然后修改nginx的conf目錄下的nginx.conf檔案,配置反向代理和負載均衡:
命令列重新加載nginx配置:nginx.exe -s reload
(3)測驗集群情況下,4.4實作的“一人一單”功能是否生效:
在VoucherOrderServiceImpl如下位置打上斷點:
以debug方式啟動兩個服務端:
我們用一個用戶發起兩次請求:
測驗結果如下:同一個用戶的兩個執行緒同時進入了物件鎖中,物件鎖失效了!
原來的資料:
現在:
說明在集群模式下出現了執行緒并發的安全問題,
4.5.2原因分析
在單機服務器的情況下:
利用互斥鎖解決了一人一單問題,確保了串行執行
在集群服務器的情況下:
如上圖,在JVM1中,synchronized修飾的是物件(UserId),synchronized依賴于monitor物件—監視器鎖來實作鎖機制,由于userId相同,鎖的監視器物件相同,因此當執行緒1來獲取鎖的時候,鎖監視器會記錄獲取鎖的物件,當執行緒2再來獲取鎖的時候,此時鎖監視器發現不是記錄的執行緒,于是執行緒2獲取互斥鎖失敗,
但是當我們做集群部署的時候,一個節點意味著一個新的tomcat,同時也意味著一個新的JVM,不同的JVM擁有各自的堆、堆疊、方法區,
JVM2中,synchronized修飾的是也是物件(UserId),它的鎖監視器和JVM1的不是同一個物件,當執行緒3來獲取鎖的時候,JVM2的鎖監視器是空的,執行緒3可以獲取互斥鎖,
綜上,鎖監視器在JVM的內部可以監視到執行緒,實作互斥,但是,如果有多個JVM,就會有多個鎖監視器,那么每一個JVM內部都會有一個執行緒獲取互斥鎖成功,這意味著在集群的情況下,可能出現執行緒的并發安全問題,
鎖底層原理
要解決上述問題,我們需要想辦法,讓多個JVM只能使用同一把鎖,
4.5.3解決方案
經過上述分析,我們已經知道在集群模式下,synchronized的鎖失效了,要想解決這個問題,需要使用分布式鎖,
分布式鎖:滿足分布式系統或集群模式下多行程可見并且互斥的鎖,
不同的分布式鎖的實作方案:
分布式鎖的核心是實作多執行緒之間互斥,滿足這一點的方式有很多,常見的有三種:
這里利用redis來實作分布式鎖,
4.5.4實作思路(基于Redis的分布式鎖)
實作分布式鎖時需要實作的兩個基本方法:
a. 獲取鎖:
- 互斥,確保只能有一個執行緒獲取鎖
- 非阻塞式:嘗試一次,成功回傳true,失敗回傳false
#添加鎖,利用setnx的互斥特性
SETNX lock thread1
#添加鎖過期時間,避免服務器宕機(非redis服務宕機)引起的死鎖
EXPIRE lock 10
此外,還要保證senx lock value和expire lock,兩個操作是原子性的,否則可能會出現添加鎖之候服務宕機的情況,這樣就會出現死鎖,因此,最好使用set命令一次性添加“鎖”和設定過期時間,
操作說明:
127.0.0.1:6379> help SET
SET key value [EX seconds] [PX milliseconds] [NX|XX]
summary: Set the string value of a key
since: 1.0.0
group: string
#獲取鎖的最終方案:添加鎖,NX是互斥,EX是設定超時時間
SET lock thread1 EX 10 NX
b. 釋放鎖:
- 手動釋放
- 超時釋放:獲取鎖時添加一個超時時間
#釋放鎖,洗掉即可
DEL key
整個流程:
4.5.5基于Redis實作分布式鎖(初級版本)
(1)定義一個類,實作下面介面,利用Redis實作分布式鎖功能
package com.hmdp.utils;
/**
* @author 李
* @version 1.0
*/
public interface ILock {
/**
* 嘗試獲取鎖
*
* @param timeoutSec 鎖持有的時間,過期后自動釋放
* @return true代表獲取鎖成功,false代表獲取鎖失敗
*/
public boolean tryLock(long timeoutSec);
/**
* 釋放鎖
*/
public void unLock();
}
(2)創建SimpleRedisLock.java
使用redis的setnx來實作分布式互斥鎖
package com.hmdp.utils;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author 李
* @version 1.0
*/
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
private static final String KEY_PREFIX = "lock:";
@Override
public boolean tryLock(long timeoutSec) {
//獲取執行緒標識
long threadId = Thread.currentThread().getId();
//獲取鎖
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);//防止空指標
}
@Override
public void unLock() {
//釋放鎖
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
(3)修改VoucherOrderServiceImpl的seckillVoucher()方法:
package com.hmdp.service.impl;
import ...
/**
* 服務實作類
*
* @author 李
* @version 1.0
*/
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result seckillVoucher(Long voucherId) {
//根據id查詢優惠券資訊
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher == null) {
return Result.fail("該優惠券不存在,請重繪!");
}
//判斷秒殺券是否在有效時間內
//若不在有效期,則回傳例外結果
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒殺尚未開始!");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒殺已經結束!");
}
//若在有效期,判斷庫存是否充足
if (voucher.getStock() < 1) {//庫存不足
return Result.fail("秒殺券庫存不足!");
}
Long userId = UserHolder.getUser().getId();
//--------------start---------------------
//創建鎖物件
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//獲取鎖
boolean isLock = lock.tryLock(1200);
//判斷是否獲取鎖成功
if (!isLock) {//獲取鎖失敗
//直接回傳錯誤,不阻塞
return Result.fail("不允許重復下單!");
}
try {
//獲取代理物件(事務)
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
//這里應該先獲取鎖,然后提交createVoucherOrder()的事務,再釋放鎖,才能確保執行緒是安全的
return proxy.createVoucherOrder(voucherId);
} finally {
//釋放鎖
lock.unLock();
}
//--------------end---------------------
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
...
}
}
(4)測驗:以debug方式啟動兩個服務端:
在如下位置打上斷點:
仍使用postman測驗:用一個用戶發起兩次請求
測驗結果:在集群模式下,只有一個請求獲取鎖成功了
redis存盤的資料:1025號用戶,執行緒id為29
4.5.6Redis分布式鎖誤刪問題
4.6Redis優化秒殺
4.7Redis訊息佇列實作異步秒殺
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/551260.html
標籤:其他
上一篇:分析查詢陳述句:EXPLAIN
下一篇:返回列表
