分散式鎖的實現(redis)
1、單機鎖
考慮在併發場景並且存在競態的狀況下,我們就要實現同步機制了,最簡單的同步機制就是加鎖。
加鎖可以幫我們鎖住資源,如記憶體中的變數,或者鎖住臨界區(執行緒中的一段程式碼),使得同一個時刻只有一個執行緒能訪問某一個區域。
如果是單例項(單程序部署),那麼單機鎖就可以滿足我們的要求了,如synchronized,ReentrantLock。
因為在一個程序中的不同執行緒可以共享這個鎖。
2、分散式鎖
但是如果場景來到了分散式系統呢?
分散式系統部署在不同的機器上,或者只是簡單的多程序部署。這樣各個程序之間無法共享同一個鎖。
這時候我們要加分散式鎖。
分散式鎖大概就是這麼一個東西:通過共享的儲存快取一個狀態值,用狀態值的變化標識鎖的佔用和釋放。
可以通過mysql,redis,zk等實現分散式鎖,這裡我們實現一個redis的。如果你用java其實使用zk會很簡單。
3、為什麼redis能用來實現分散式鎖?
1)Redis是單程序單執行緒模式
redis實現為單程序單執行緒模式,這樣多個客戶端並不存在競態關係。
2)原子性原語
redis提供了可以實現原子操作的原語如setnx、getset等。
setnx
1)SETNX key value 將 key 的值設為 value ,當且僅當 key 不存在。 若給定的 key 已經存在,則 SETNX 不做任何動作。 SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。 可用版本: >= 1.0.0 時間複雜度: O(1) 返回值: 設定成功,返回 1 。 設定失敗,返回 0 。
getset
GETSET key value
將給定 key 的值設為 value ,並返回 key 的舊值(old value)。
當 key 存在但不是字串型別時,返回一個錯誤。
可用版本:
>= 1.0.0
時間複雜度:
O(1)
返回值:
返回給定 key 的舊值。
當 key 沒有舊值時,也即是, key 不存在時,返回 nil 。
4、實現
package com.xiaoju.dqa.fusor.utils; import com.xiaoju.dqa.fusor.client.RedisClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class DistributeLockUtil { // 鎖超時時間, 防止死鎖 private static final long LOCK_TIMEOUT = 60; @Autowired private RedisClient redisClient; private boolean locked = false; public boolean lock(String key) { String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000); /* * setnx 返回1 * 說明: 1)key不存在, 2)成功寫入鎖, 並更新鎖的生存時間 * 也就是get鎖 * */ if (redisClient.setnx(key, expireTime) == 1) { locked = true; return true; } /* * 沒有get鎖, 下面進入判斷鎖超時邏輯 * */ String currentExpireTime = redisClient.get(key); /* * 鎖生存時間已經過了, 說明鎖已經超時 * */ if (Long.parseLong(currentExpireTime) < System.currentTimeMillis()) { String oldValueStr = redisClient.getSet(key, expireTime); /* * 判斷鎖生存時間和你改的寫那個時間是否相等 * 相當於你競爭了一個更新鎖 * */ if (oldValueStr.equals(currentExpireTime)) { locked = true; return true; } } return false; } public void release(String key) { if (locked) { redisClient.del(key); locked = false; } } }
5、死鎖
為了解決死鎖,這裡設定了鎖的超時時間。
private static final long LOCK_TIMEOUT = 60;
並通過setnx時更新鎖生存時間來維護鎖超時的判定。
String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
...
if (redisClient.setnx(key, expireTime) == 1) {
...
}
...
String oldValueStr = redisClient.getSet(key, expireTime);
...
為什麼要使用這種方式,而不是expire呢?
因為setnx和expire不能作為一個原子性的操作存在,設想如果setnx之後,在執行expire之前出現了異常,那麼鎖將沒有超時時間。也就是死鎖。
6、解決鎖超時引入的競態
設想三個客戶端,C0,C1,C2
如果C0持有鎖並且崩潰,鎖沒有釋放。
C1和C2同時發現了鎖超時。
然後都通過getset去拿到了舊值,在對比了舊值和之前值之後,如果相等,那麼說明“我”成功修改了舊值,那麼我就拿到了鎖。
7、 時鐘同步
我們看到foo.lock的value值為時間戳,所以要在多客戶端情況下,保證鎖有效,一定要同步各伺服器的時間,如果各伺服器間,時間有差異。時間不一致的客戶端,在判斷鎖超時,就會出現偏差,從而產生競爭條件。 鎖的超時與否,嚴格依賴時間戳,時間戳本身也是有精度限制,假如我們的時間精度為秒,從加鎖到執行操作再到解鎖,一般操作肯定都能在一秒內完成。這樣的話,我們上面的CASE,就很容易出現。所以,最好把時間精度提升到毫秒級。這樣的話,可以保證毫秒級別的鎖是安全的。
8、一些處理不了的情況
設想三個客戶端,C0,C1,C2
如果C0持有鎖很長,鎖已經超時。這時候有C1,C2判斷鎖超時了,然後通過超時競爭,C1拿到了鎖。
這時C0醒了過來,刪除了C1的鎖。
這時,C1認為自己獨佔了鎖,其他的程序也進入了競爭鎖的情況
對於這種情況,這裡是沒有提供解決辦法的。
思路是:你降級你的鎖,比如給你的鎖加上uuid,對不同的業務或者不同的session加上對應粒度的鎖。
可以看看這篇部落格。
http://www.cnblogs.com/kangoroo/p/6953187.html