
3.2. 分布式鎖的實作
随着業務發展的需要,原單體單機部署的系統被演化成分布式叢集系統後,由于分布式系統多線程、多程序并且分布在不同機器上,這将使原單機部署情況下的并發控制鎖政策失效,單純的Java API并不能提供分布式鎖的能力。為了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的通路,這就是分布式鎖要解決的問題!
分布式鎖主流的實作方案:
基于資料庫實作分布式鎖
基于緩存(Redis等)
基于Zookeeper
每一種分布式鎖解決方案都有各自的優缺點:
性能:redis最高
可靠性:zookeeper最高
這裡,我們就基于redis實作分布式鎖。
3.2.1. 基本實作
借助于redis中的指令setnx(key, value),key不存在就新增,存在就什麼都不做。同時有多個用戶端發送setnx指令,隻有一個用戶端可以成功,傳回1(true);其他的用戶端傳回0(false)。
主要使用Redis Setnx 指令
在指定的 key 不存在時,為 key 設定指定的值
設定成功,傳回 1 。 設定失敗,傳回 0
redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 設定成功
(integer) 1
redis> SETNX job "code-farmer" # 嘗試覆寫 job ,失敗
(integer) 0
redis> GET job # 沒有被覆寫
"programmer"
java代碼
public void testLock() {
// 執行redis的setnx指令
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 5, TimeUnit.SECONDS);
// 判斷是否拿到鎖
if (lock) {
// 執行業務邏輯代碼
// ...
// 釋放鎖資源 (保證擷取值和删除操作的原子性) LUA腳本保證删除的原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1]) else return 0 end";
this.redisTemplate.execute(new DefaultRedisScript<>(script),
Arrays.asList("lock"), Arrays.asList(uuid));
// if (StrUtil.equals(uuid,redisTemplate.opsForValue().get("lock"))){
// redisTemplate.delete("lock");
// }
} else {
// 其他請求嘗試擷取鎖
testLock();
}
}
為了確定分布式鎖可用,我們至少要確定鎖的實作同時滿足以下四個條件:
互斥性。在任意時刻,隻有一個用戶端能持有鎖。
不會發生死鎖。即使有一個用戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他用戶端能加鎖。
解鈴還須系鈴人。加鎖和解鎖必須是同一個用戶端,用戶端自己不能把别人加的鎖給解了。
加鎖和解鎖必須具有原子性。