生產級Redis 高併發分佈式鎖實戰1:高併發分佈式鎖如何實現

高併發場景:秒殺商品。

秒殺一般出現在商城的促銷活動中,指定了一定數量(比如: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: 一隻阿木木

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章