分散式鎖的實現之 redis 篇

為什麼需要分散式鎖

引入經典的秒殺情景,100件商品供客戶搶。如果是單機版的話,我們使用synchronized 或者 lock 都可以實現執行緒安全。但是如果多個伺服器的話,synchronized 和 lock 就不管用了(廢話,怎麼可能管用,都不在同一段程式碼了)。

分散式鎖就是被設計出來實現多個伺服器的執行緒安全。

很容易想到的方案是把共享變數(鎖)抽取出來放在一個公共的資料庫里(Redis、Memchhed)里,所有的伺服器通過這個公共的資源實現數據的一致性,防止超賣。

具體實現

分散式鎖的實現方式有:Memchched分散式鎖、Redis分散式鎖、Zookeeper分散式鎖,這裡我們以Redis分散式鎖為例,Redis分散式鎖也是現在使用得最多的

1. 思路

  • setnx加鎖

    setnx是實現分散式的核心,意思是只有當前key不存在才返回1,當前key存在返回0

    這個key就是我們的「鎖」,只有執行緒獲得鎖才能繼續執行,執行完del這個key相當於解鎖操作。這個就是redis實現分散式鎖的核心,怎麼樣,很好理解吧

  • del解鎖

2. 第一個問題:鎖無法被釋放

試想一下,如果你執行完set命令伺服器宕機了,來不及del解鎖,那麼這個鎖永遠無法被釋放,其他執行緒無法執行。

解決方法是key必須設置一個超時時間,即使沒有被顯示釋放,也在超時後自動釋放。

redis為我們提供了這個命令設置超時時間

  • expire key ttl 秒為單位
  • pexpire key ttl 毫秒為單位
  • expireat key timestamp
  • pexpireat key timestamp

因此加鎖的操作變成:

setnx lock 1
expire lock 10

但是這兩個操作不保證原子性(Redis單條操作保證原子性),如果加完鎖還沒設置過期時間伺服器就宕機了,同樣會導致死鎖,因此加鎖整個操作必須保證原子性。

redis提供了set+過期時間的原子操作

set lock 1 EX 10 NX
// 最終的加鎖命令

3. 第二個問題:錯誤釋放鎖

第二個問題,如果執行緒執行時間超過TTL,當前鎖被自動銷毀

但是等執行緒執行完了,原來的del方法還會執行,它就會去執行解鎖操作,把其他執行緒佔用的鎖給del了,這會產生非常嚴重的問題

String REDIS_Lock="lock";
String value=1;
try{
    redisUtil.setLockDistribute(REDIS_LOCK,1,10);
    ......業務邏輯
        
}finally{
    // 這個操作有可能會誤刪鎖
    redisUtil.del(REDIS_LOCK);
}

解決方案是key的value不再是默認的了

String REDIS_Lock="lock";
String value=UUID.randomUUID().toString()+Thread.currentThread().getname();
try{
    redisUtil.setLockDistribute(REDIS_LOCK,1,10);
    ......業務邏輯
        
}finally{
    // 先判斷後刪除
    if(redisUtil.get(REDIS_LOCK).equals(value)){
        redisUtil.del(REDIS_LOCK);
    }
    
}

這樣寫其實還有個問題,判斷和刪除無法保證原子性,還是有可能誤刪。因此解鎖我們使用lua腳本來保證原子性:工具類有實現lua腳本的方法。

//lua腳本刪除key原子操作
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

(解鎖操作也可用事務來保證原子性,應付面試,實戰還是lua腳本)

4. 第三個問題:超時解鎖導致並發

加鎖和解鎖操作我們都搞定了,但是還有一個問題:如果你的執行緒執行時間超過ttl過期時間,鎖還是被釋放了,其他執行緒可以和次執行緒並發執行,這是我們並不想看到的。

因此我們要為ttl延時

我們可以讓獲得鎖的執行緒開啟一個守護執行緒,用來給快要過期的鎖「續航」。

image-20210502230844016

5. 集群環境下可能出現的問題

redis集群環境,多個master,多個slave的情況下:

當主節點掛掉時,從節點會取而代之,但客戶端無明顯感知。當客戶端 A 成功加鎖,指令還未同步,此時主節點掛掉,從節點提升為主節點,新的主節點沒有鎖的數據,當客戶端 B 加鎖時就會成功。

也就是主結點加了鎖就宕機了,從節點還沒同步,當該從節點提升為主節點時就會出錯。

image-20210502231255950

解決方案我也不清楚….以後碰到再找資料

開源框架Redisson

上面的流程如果手寫的話會要人老命,開源框架Redisson幫我們擺平一切,現在用得十分多

直接上程式碼:

// 注入redisson
public Redisson redisson(){
    Config config=new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
    return Redisson.create(config);

}
@Autowired
Redisson son;

String REDIS_Lock="lock";
String value=UUID.randomUUID().toString()+Thread.currentThread().getname();
RLock lock=son.getLock();
try{
    lock.lock();
    ......業務邏輯
        
}finally{
   	lock.unlock();
}

// 這段程式碼會解決上述三個問題,集群環境下redis分散式鎖的實現

結語

分散式鎖看起來難其實原理還是很簡單的,沒事多看看官方文檔,講得挺細緻的

參考