分散式鎖的實現之 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延時
我們可以讓獲得鎖的執行緒開啟一個守護執行緒,用來給快要過期的鎖「續航」。
5. 集群環境下可能出現的問題
redis集群環境,多個master,多個slave的情況下:
當主節點掛掉時,從節點會取而代之,但客戶端無明顯感知。當客戶端 A 成功加鎖,指令還未同步,此時主節點掛掉,從節點提升為主節點,新的主節點沒有鎖的數據,當客戶端 B 加鎖時就會成功。
也就是主結點加了鎖就宕機了,從節點還沒同步,當該從節點提升為主節點時就會出錯。
解決方案我也不清楚….以後碰到再找資料
開源框架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分散式鎖的實現
結語
分散式鎖看起來難其實原理還是很簡單的,沒事多看看官方文檔,講得挺細緻的
參考
- 分散式鎖的實現之 redis 篇
- 漫畫:什麼是分散式鎖?
- [通俗講解分散式鎖,看完不懂算作者輸](//www.zhihu.com/search?type=content&q=分散式鎖)
- 尚矽谷周陽-大廠面試題第二季