高併發場景:秒殺商品。
秒殺一般出現在商城的促銷活動中,指定了一定數量(比如:1000個)的商品(比如:手機),以極低的價格(比如:0.1元),讓大量用戶參與活動,但只有極少數用戶能夠購買成功.
示例代碼
@RestController
public class IndexController {
@autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate
@RequestMapper("/stock/deduct_stock")
public String deductStock(){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + ""); // jedis.set(key,value)
System.out.print("======商品扣減成功,剩餘庫存:"+ realStock);
} else {
System.out.print("======商品扣減失敗======");
}
return "---=== end ===---";
}
}
問題
假設10個線程同時扣減1000個商品,應該剩餘990 個商品,但實際扣減剩餘999個,發生超賣問題。
- 解決方案
加鎖。synchronized(this)
jdk 內置鎖在分佈式環境下並不能保證成功。
模擬高併發場景,部署測試環境。
可用jMeter 工具進行壓力測試。
添加ThreadGroup,並在欄目下添加 Http Request。配置併發計劃。
可選擇添加Aggregate Report 報告。
完整代碼:
@RequestMapper("/stock/deduct_stock")
public String deductStock(){
synchronized(this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.print("======商品扣減成功,剩餘庫存:"+ realStock);
} else {
System.out.print("======商品扣減失敗======");
}
}
return "---=== end ===---";
}
啓動多臺服務器,測試日誌結果可知,發生超賣問題。
添加分佈式鎖
redis 命令setnx key value
setnx 是“set if not exists” 的縮寫。
含義是:
- 將key 的值設爲value, 當且僅當key 不存在。
- 若給定的key 已經存在,則setnx 不做任何操作。
注:加鎖完成後,要刪除鎖。
完整代碼:
String lockKey = "lock:product_100";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lock,"amumu")
if (!result){
return "---=== error_code ---===";
}
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.print("======商品扣減成功,剩餘庫存:"+ realStock);
} else {
System.out.print("======商品扣減失敗======");
}
stringRedisTemplate.delete(lockkey);
return "---=== end ===---";
分佈式鎖下的原子性問題優化
發現問題
- 在set過程中程序異常,則程序無法繼續執行,造成死鎖。
//獲取異常並拋異常
try{
} finally{
}
- 在set 過程中,系統宕機,或系統重啓。
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,"ammumu");
//添加過期時間
stringRedisTemplate.expire(lockkey,10,TimeUnit.SECONDS);
上述代碼執行過程中,程序異常情況下也會導致系統宕機,發生原子性問題,
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,"amumu",10, TimeUnit.SECONDS);
分佈式鎖的失效情況優化
- 假設 10s 的過期時間。
假設多個請求,每個程序執行超過10s, 此時鎖已過期。此時程序執行刪除鎖,導致此時鎖失效。
分析問題:加鎖過程,被其他線程執行刪鎖。
解決方案:加上鎖標識,刪除時判斷是否是同一標識。
String clientId = UUID.randomUUID().toString();
// 加上cliendId 鎖標識
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,clientId, 10, TimeUnits.SECONDS);
//刪除鎖時,進行判斷
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockkey);
}
- 假設執行刪除鎖前,鎖快過期未過期。下一個併發線程執行加鎖操作,此時上一個併發線程,執行了刪除操作。鎖依然會失效。
解決方案:鎖續命。如redisson
參考:github.com/redisson/redisson
每隔10s 檢查是否還持有鎖,如果持有,則延長30s鎖的時間
參考:redis lua 腳本
RLock redissonLock = redisson.getLock(lock); // 獲取鎖對象
redissonLock.lock(); // 加鎖 相當於 setIfAbsent(lockkey,"amumu",10, TimeUnit.SECONDS);
redissonLock.unlock();
完整代碼:
@autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate
@RequestMapper("/stock/deduct_stock")
public String deductStock(){
String lockKey = "lock:product_101";
RLock redissonLock = redisson.getLock(lockKey);
redissonLock.lock(); // 加鎖
try{
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0){
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock + "");
System.out.print("======商品扣減成功,剩餘庫存:"+ realStock);
} else {
System.out.print("======商品扣減失敗======");
}
}finally{
redissonLock.unlock();
}
return "---=== end ===---";
}
鎖續命業務優化
-
問題1 :線程1 加鎖,此時未同步給從結點,主結點掛了。此時從結點很可能會被選爲主結點。這時一個線程3,同時請求此操作,此時線程3 操作新選爲主結點redis 加鎖,此時程序繼續執行,執行有併發安全問題的代碼。
-
問題2 :redis 語義上,是把高併發場景下的問題串行化了,這當然不會有併發問題,但是對整個系統的性能有影響。當然redis 的性能已經是非常不錯了。
-
問題3:redissonLock.unLock() 底層默認的是非公平的搶鎖機制,while 循環嘗試去加鎖。
redis 與 zookeeper 分佈式集羣方案對比
根據CAP 理論:
redis 集羣更多是保證AP, 即可用性。
zookeeper 集羣更多是保證CP, 即一致性,ZAB寫數據機制保證,不會有鎖丟失的問題。
redlock 實解決redis 集羣環境鎖丟失問題
想用redis 的高性能,但是又不想丟失鎖,有很多方法。
比如 Redlock 實現。
java client 加鎖三個以上的鎖,超過半數redis 節點加鎖成功纔算加鎖成功。
- 問題:千萬不要用從節點。要保證集羣高可用,多加幾個節點。
- 問題:持久化。一般用aof 做持久化,假設設置1s 持久化一次。
這樣就設置一個key,設置2個key 時系統宕機或重啓。client2 執行請求,key3 加鎖成功,key2 重啓後加鎖成功,則加鎖成功。系統繼續執行的是併發安全問題的代碼。如果用時時持久化設置,則性能會大大降低,還不如用zookeeper.
高併發分佈式鎖如何實現
分段加鎖邏輯。
未完待續
by: 一隻阿木木