【329期】如何利用redis分布式鎖,解決秒殺場景下的訂單超賣問題
1. 秒殺場景
Controller層:
@RestController
@RequestMapping("/skill")
@Slf4j
public class SecKillController {
@Autowired
private SecKillService secKillService;
//查詢秒殺活動(dòng)特價(jià)商品的信息
@GetMapping("/query/{productId}")
public String query(@PathVariable String productId)throws Exception {
return secKillService.querySecKillProductInfo(productId);
}
//秒殺
@GetMapping("/order/{productId}")
public String skill(@PathVariable String productId)throws Exception {
log.info("@skill request, productId:" + productId);
secKillService.orderProductMockDiffUser(productId);
return secKillService.querySecKillProductInfo(productId);
}
}
Service層:
@Service
public class SecKillServiceImpl implements SecKillService {
private static final int TIMEOUT = 10 * 1000; //超時(shí)時(shí)間 10s
@Autowired
private RedisLock redisLock;
// 雅詩蘭黛特價(jià)小棕瓶,限量100000份
static Map<String,Integer> products;
static Map<String,Integer> stock;
static Map<String,String> orders;
static {
//模擬多個(gè)表,商品信息表,庫存表,秒殺成功訂單表
products = new HashMap<>();
stock = new HashMap<>();
orders = new HashMap<>();
//商品Id---商品庫存
products.put("123456", 100000);
//商品id---商品庫存
stock.put("123456", 100000);
}
private String queryMap(String productId) {
return "雅詩蘭黛小棕瓶特價(jià),限量份"
+ products.get(productId)
+" 還剩:" + stock.get(productId)+" 份"
+" 該商品成功下單用戶數(shù)目:"
+ orders.size() +" 人" ;
}
@Override
public String querySecKillProductInfo(String productId) {
return this.queryMap(productId);
}
//秒殺的邏輯:可以在該方法生加上Synchronized解決超賣
@Override
public void orderProductMockDiffUser(String productId) {
//1.查詢該商品庫存,為0則活動(dòng)結(jié)束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活動(dòng)結(jié)束");
}else {
//2.下單(模擬不同用戶openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.減庫存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.更新庫存
stock.put(productId,stockNum);
}
}
}
使用壓測工具測試結(jié)果:雅詩蘭黛小棕瓶特價(jià),限量份100000 還剩:99938份 該商品成功下單用戶數(shù)目:646人
很明顯訂單超賣了,如何解決?可以在秒殺方法上加上Synchronized,但是只適合單點(diǎn)服務(wù)器,且性能低
2. Redis分布式鎖解決訂單超賣
2.1 兩個(gè)命令介紹
業(yè)務(wù)場景:
天貓雙11熱賣過程中,對已經(jīng)售罄的貨物追加補(bǔ)貨,且補(bǔ)貨完成。客戶購買熱情高漲, 3秒內(nèi)將所有商品購買完畢。本次補(bǔ)貨已經(jīng)將庫存全部清空,如何避免最后一件商品不被多人同時(shí)購買?【超賣問題】就是說如果只剩一件商品,但是有5個(gè)人要買,如何保證不被超賣???
業(yè)務(wù)分析:
使用watch監(jiān)控一個(gè)key有沒有改變已經(jīng)不能解決問題,此處要監(jiān)控的是具體數(shù)據(jù) ,我們要監(jiān)控的是商品的數(shù)量什么時(shí)候到1,這個(gè)商品的數(shù)量是一直變化的,不可能別每次變化,都放棄執(zhí)行。
解決方案:
使用 setnx 設(shè)置一個(gè)公共鎖:setnx lock-key value,操作完畢通過del操作釋放鎖
利用setnx命令的返回值特征,有值則返回設(shè)置失敗,無值則返回設(shè)置成功
對于返回設(shè)置成功的,擁有控制權(quán)進(jìn)行下一步的具體業(yè)務(wù)操作,對于返回設(shè)置失敗的,不具有控制權(quán),排隊(duì)或等待
127.0.0.1:6379> set num 10
OK
127.0.0.1:6379> setnx lock-num 1 -- 加鎖
(integer) 1
127.0.0.1:6379> incrby num -1
(integer) 9
127.0.0.1:6379> del lock-num -- 釋放鎖
(integer) 1
127.0.0.1:6379> setnx lock-num 1 -- 當(dāng)前客戶端加鎖
(integer) 1
127.0.0.1:6379> setnx lock-num 1 -- 其他客戶端獲取不到鎖
(integer) 0
死鎖: 如果加了鎖,但是沒有釋放,就會(huì)導(dǎo)致死鎖,其他客戶端一直獲取不到鎖。
使用expire為鎖key添加時(shí)間限定,到時(shí)間不釋放,放棄鎖
由于操作通常都是微秒或毫秒級,因此該鎖定時(shí)間不宜設(shè)置過大。具體時(shí)間需要業(yè)務(wù)測試后確認(rèn)。推薦:Java進(jìn)階視頻資源
expire lock-key second pexpire lock-key milliseconds
127.0.0.1:6379> set name 123
OK
127.0.0.1:6379> setnx lock-name 1 -- 鎖的名稱key
(integer) 1
127.0.0.1:6379> expire lock-name 20 -- 使用expire為鎖key添加時(shí)間限定
(integer) 1
127.0.0.1:6379> get name
"123"
GETSET key value
將給定 key 的值設(shè)為 value ,并返回 key 的舊值(old value)。
redis> GETSET db mongodb # 沒有舊值,返回 nil
(nil)
redis> GET db
"mongodb"
redis> GETSET db redis # 返回舊值 mongodb
"mongodb"
redis> GET db
"redis"
2.2 RedisLock
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加鎖
* @param key:productId
* @param value 當(dāng)前時(shí)間+超時(shí)時(shí)間
*/
public boolean lock(String key, String value) {
//setnx----對應(yīng)方法 setIfAbsent(key, value),如果可以加鎖返回true,不可以加鎖返回false
if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
//下面這段代碼時(shí)為了解決可能出現(xiàn)的死鎖情況
String currentValue = redisTemplate.opsForValue().get(key);
//如果鎖過期
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()) {
//獲取上一個(gè)鎖的時(shí)間:重新設(shè)置鎖的過期時(shí)間value,并返回上一個(gè)過期時(shí)間
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
//currentValue =2020-12-28,兩個(gè)線程的value=2020-12-29,只會(huì)有一個(gè)線程拿到鎖
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
//解鎖
public void unlock(String key, String value) {
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e) {
log.error("【redis分布式鎖】解鎖異常, {}", e);
}
}
}
2.3 將redis分布式鎖應(yīng)用于秒殺業(yè)務(wù)
@Override
public void orderProductMockDiffUser(String productId) {
//加鎖
//鎖的過期時(shí)間為當(dāng)前時(shí)間+過期時(shí)長
long time = System.currentTimeMillis()+TIMEOUT;
if(!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(101,"人太多,稍后再來");
}
//1.查詢該商品庫存,為0則活動(dòng)結(jié)束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活動(dòng)結(jié)束");
}else {
//2.下單(模擬不同用戶openid不同)
orders.put(KeyUtil.genUniqueKey(),productId);
//3.減庫存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.更新庫存
stock.put(productId,stockNum);
}
//解鎖
redisLock.unlock(productId,String.valueOf(time));
}
2.4 分析RedisLock
重點(diǎn)分析加鎖邏輯,有兩個(gè)邏輯需要考慮:
public boolean lock(String key, String value) {
if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
//下面的代碼是為了解決可能出現(xiàn)的死鎖的情況????
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()) {
//下面這個(gè)邏輯又怎么理解????
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
為了解決可能出現(xiàn)的死鎖的情況????
//秒殺業(yè)務(wù)方法
@Override
public void orderProductMockDiffUser(String productId) {
//加鎖
//鎖的過期時(shí)間為當(dāng)前時(shí)間+過期時(shí)長
long time = System.currentTimeMillis()+TIMEOUT;
if(!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(101,"人太多,稍后再來");
}
//1.查詢該商品庫存,為0則活動(dòng)結(jié)束。
int stockNum = stock.get(productId);
if(stockNum == 0) {
throw new SellException(100,"活動(dòng)結(jié)束");
}else {
//2.下單
orders.put(KeyUtil.genUniqueKey(),productId);
//3.減庫存
stockNum =stockNum-1;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//4.更新庫存
stock.put(productId,stockNum);
}
//解鎖
redisLock.unlock(productId,String.valueOf(time));
}
假如我們將中間那段邏輯去掉會(huì)出現(xiàn)聲明情況???
public boolean lock(String key, String value) {
if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
return false;
}
① 線程A執(zhí)行秒殺的業(yè)務(wù)邏輯方法,并對這個(gè)方法加了鎖,key=proeuctId,value=加鎖時(shí)間+過期時(shí)長,然后開始執(zhí)行下單----》減庫存-----》更新庫存等操作,如果在執(zhí)行的過程中,這段代碼發(fā)生了異常,那么線程A是不會(huì)釋放鎖的,導(dǎo)致其他線程都無法獲取到鎖導(dǎo)致死鎖的產(chǎn)生,所以下面的邏輯是很有必要加的,即如果當(dāng)前時(shí)間晚于鎖的過期時(shí)間,那么就會(huì)向下走if()條件:
//下面的代碼是為了解決可能出現(xiàn)的死鎖的情況????
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()) {
//下面這個(gè)邏輯又怎么理解????
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
② 可是下面的if()條件怎么理解?
currentValue=2020-12-18
假如現(xiàn)在兩個(gè)線程A和B同時(shí)執(zhí)行l(wèi)ock()方法,也就是這兩個(gè)線程的value是完全相同的,都為value=2020-12-19,而他們都執(zhí)行 String oldValue = redisTemplate.opsForValue().getAndSet(key, value);,會(huì)有一個(gè)先執(zhí)行一個(gè)后執(zhí)行:
假如線程A先執(zhí)行,返回的oldValue=2020-12-18,同時(shí)設(shè)置value = 2020-12-19,由于oldvalue=currentValue返回true,即A線程加了鎖;
此時(shí)B線程繼續(xù)執(zhí)行 ,返回的oldValue=2020-12-19,oldvalue!=currentValue,返回false,加鎖失敗
所以這段代碼的邏輯是只會(huì)讓一個(gè)線程加鎖
public boolean lock(String key, String value) {
if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()) {
//下面這個(gè)邏輯又怎么理解????
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
感謝閱讀,希望對你有所幫助 :)
來源:hengheng.blog.csdn.net/article/details/107827649
最近給大家找了 通用權(quán)限系統(tǒng)
資源,怎么領(lǐng)取?
掃二維碼,加我微信,回復(fù):通用權(quán)限系統(tǒng)
注意,不要亂回復(fù) 沒錯(cuò),不是機(jī)器人 記得一定要等待,等待才有好東西
