分佈式鎖是控制分佈式系統同步訪問共享資源的一種方式。java
一、在分佈式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行; 二、高可用的獲取鎖與釋放鎖; 三、高性能的獲取鎖與釋放鎖; 四、具有可重入特性; 五、具有鎖失效機制,防止死鎖; 六、具有非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。node
目前分佈式鎖的實現方式主要採用如下三種:linux
儘管有這三種方案,可是不一樣的業務也要根據本身的狀況進行選型,他們之間沒有最好只有更適合!git
基於數據庫的實現方式的核心思想是:在數據庫中建立一個表,表中包含方法名等字段,並在方法名字段上建立惟一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖。github
1.建立一個表:redis
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名',
`desc` varchar(255) NOT NULL COMMENT '備註信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
複製代碼
2.若是要執行某個方法,則使用這個方法名向數據庫總插入數據:算法
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');
複製代碼
由於咱們對method_name
作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。sql
3.成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖:shell
delete from method_lock where method_name ='methodName';
複製代碼
注意:這只是使用基於數據庫的一種方法,使用數據庫實現分佈式鎖還有不少其餘的玩法!數據庫
使用基於數據庫的這種實現方式很簡單,可是對於分佈式鎖應該具有的條件來講,它有一些問題須要解決及優化:
一、由於是基於數據庫實現的,數據庫的可用性和性能將直接影響分佈式鎖的可用性及性能,因此,數據庫須要雙機部署、數據同步、主備切換;
二、不具有可重入的特性,由於同一個線程在釋放鎖以前,行數據一直存在,沒法再次成功插入數據,因此,須要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;
三、沒有鎖失效機制,由於有可能出現成功插入數據後,服務器宕機了,對應的數據沒有被刪除,當服務恢復後一直獲取不到鎖,因此,須要在表中新增一列,用於記錄失效時間,而且須要有定時任務清除這些失效的數據;
四、不具有阻塞鎖特性,獲取不到鎖直接返回失敗,因此須要優化獲取邏輯,循環屢次去獲取。
五、在實施的過程當中會遇到各類不一樣的問題,爲了解決這些問題,實現方式將會愈來愈複雜;依賴數據庫須要必定的資源開銷,性能問題須要考慮。
一、選用Redis實現分佈式鎖緣由:
(1)Redis有很高的性能; (2)Redis命令對此支持較好,實現起來比較方便
二、實現思想:
(1)獲取鎖的時候,使用setnx加鎖,並使用expire命令爲鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值爲一個隨機生成的UUID,經過此在釋放鎖的時候進行判斷。
(2)獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
(3)釋放鎖的時候,經過UUID判斷是否是該鎖,如果該鎖,則執行delete進行鎖釋放。
三、使用命令介紹:
SETNX:
SETNX key val:#當且僅當key不存在時,set一個key爲val的字符串,返回1;若key存在,則什麼都不作,返回0。
複製代碼
EXPIRE:
expire key timeout:#爲key設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。
複製代碼
DELETE:
delete key:#刪除key
複製代碼
若是在 setnx 和 expire 之間服務器進程忽然掛掉了,多是由於機器掉電或者是被人爲殺掉的,就會致使 expire 得不到執行,也會形成死鎖。因此可使用如下指令使得setnx和expire在同一條指令中執行:
set lock:codehole value ex 5 nx
複製代碼
四、實現代碼:
//可重入鎖
public class RedisWithReentrantLock {
private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>();
private Jedis jedis;
public RedisWithReentrantLock(Jedis jedis) {
this.jedis = jedis;
}
private boolean _lock(String key) {
return jedis.set(key, "", "nx", "ex", 5L) != null;
}
private void _unlock(String key) {
jedis.del(key);
}
private Map<String, Integer> currentLockers() {
Map<String, Integer> refs = lockers.get();
if (refs != null) {
return refs;
}
lockers.set(new HashMap<>());
return lockers.get();
}
public boolean lock(String key) {
Map<String, Integer> refs = currentLockers();
Integer refCnt = refs.get(key);
if (refCnt != null) {
refs.put(key, refCnt + 1);
return true;
}
boolean ok = this._lock(key);
if (!ok) {
return false;
}
refs.put(key, 1);
return true;
}
public boolean unlock(String key) {
Map<String, Integer> refs = currentLockers();
Integer refCnt = refs.get(key);
if (refCnt == null) {
return false;
}
refCnt -= 1;
if (refCnt > 0) {
refs.put(key, refCnt);
} else {
refs.remove(key);
this._unlock(key);
}
return true;
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
RedisWithReentrantLock redis = new RedisWithReentrantLock(jedis);
System.out.println(redis.lock("codehole"));
System.out.println(redis.lock("codehole"));
System.out.println(redis.unlock("codehole"));
System.out.println(redis.unlock("codehole"));
}
}
/** * 分佈式鎖的簡單實現代碼 * Created by liuyang on 2017/4/20. */
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/** * 加鎖 * @param lockName 鎖的key * @param acquireTimeout 獲取超時時間 * @param timeout 鎖的超時時間 * @return 鎖標識 */
public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 獲取鏈接
conn = jedisPool.getResource();
// 隨機生成一個value
String identifier = UUID.randomUUID().toString();
// 鎖名,即key值
String lockKey = "lock:" + lockName;
// 超時時間,上鎖後超過此時間則自動釋放鎖
int lockExpire = (int) (timeout / 1000);
// 獲取鎖的超時時間,超過這個時間則放棄獲取鎖
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (jedis.set(key, "", "nx", "ex", 5L) != null) {
retIdentifier = identifier;
return retIdentifier;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/** * 釋放鎖 * @param lockName 鎖的key * @param identifier 釋放鎖的標識 * @return */
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 監視lock,準備開始事務
conn.watch(lockKey);
// 經過前面返回的value值判斷是否是該鎖,如果該鎖,則刪除,釋放鎖
if (identifier.equals(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) {
continue;
}
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
}
複製代碼
使用這種方式實現分佈式鎖在集羣模式下會有必定的問題,好比在 Sentinel 集羣中,主節點掛掉時,從節點會取而代之,客戶端上卻並無明顯感知。原先第一個客戶端在主節點中申請成功了一把鎖,可是這把鎖尚未來得及同步到從節點,主節點忽然掛掉了。而後從節點變成了主節點,這個新的節點內部沒有這個鎖,因此當另外一個客戶端過來請求加鎖時,當即就批准了。這樣就會致使系統中一樣一把鎖被兩個客戶端同時持有,不安全性由此產生。
爲了解決這個問題,Antirez 發明了 Redlock 算法,加鎖時,它會向過半節點發送 set(key, value, nx=True, ex=xxx)
指令,只要過半節點 set
成功,那就認爲加鎖成功。釋放鎖時,須要向全部節點發送 del
指令。不過 Redlock 算法還須要考慮出錯重試、時鐘漂移等不少細節問題,同時由於 Redlock 須要向多個節點進行讀寫,意味着相比單實例 Redis 性能會降低一些。
在使用zookeeper實現分佈式鎖的以前,須要先了解zookeeper的兩個特性,第一個是zookeeper的節點類型,第二就是zookeeper的watch機制:
zookeeper的節點類型:
PERSISTENT 持久化節點
PERSISTENT_SEQUENTIAL 順序自動編號持久化節點,這種節點會根據當前已存在的節點數自動加 1
EPHEMERAL 臨時節點, 客戶端session超時這類節點就會被自動刪除
EPHEMERAL_SEQUENTIAL 臨時自動編號節點
zookeeper的watch機制:
Znode發生變化(Znode自己的增長,刪除,修改,以及子Znode的變化)能夠經過Watch機制通知到客戶端。那麼要實現Watch,就必須實現org.apache.zookeeper.Watcher接口,而且將實現類的對象傳入到能夠Watch的方法中。Zookeeper中全部讀操做(getData(),getChildren(),exists())均可以設置Watch選項。Watch事件具備one-time trigger(一次性觸發)的特性,若是Watch監視的Znode有變化,那麼就會通知設置該Watch的客戶端。
定義鎖:
在一般的java併發編程中,有兩種常見的方式能夠用來定義鎖,分別是synchronized機制和JDK5提供的ReetrantLock。然而,在zookeeper中,沒有相似於這樣的API能夠直接使用,而是經過Zookeeper上的數據節點來表示一個鎖,例如/exclusive_lock/lock節點就能夠定義爲一個鎖。
獲取鎖:
在須要獲取排他鎖的時候,全部的客戶端都會試圖經過調用create()接口,在/exclusive_lock節點下建立臨時子節點/exclusive_lock/lock。zookeeper會保證在全部客戶端中,最終只有一個客戶端可以建立成功,那麼就能夠認爲該客戶端得到了鎖。同時,全部沒有得到鎖的客戶端就須要到/exclusive_lock節點上註冊一個子節點變動的Watcher監聽,以便實時監聽到lock節點的變動狀況。
釋放鎖:
在定義鎖部分,咱們已經提到,/exclusive_lock/lock是一個臨時節點,所以在如下兩種狀況下,都有可能釋放鎖。
1.當前獲取鎖的客戶端發生宕機,那麼Zookeeper上的這個臨時節點就會被移除。
2.正常執行完業務邏輯以後,客戶端就會主動將本身建立的臨時節點刪除
不管在什麼狀況下移除了lock節點,Zookeeper都會通知全部在/exclusive_lock節點上註冊了子節點變動Watcher監聽的客戶端。這些客戶端在接收到通知後,再次從新發起分佈式鎖獲取,即重複「獲取鎖」的過程:
定義鎖:
和排他鎖同樣,一樣是經過zookeeper上的數據節點來表示一個鎖,是一個相似於"/shared_lock/[hostname]-請求類型-序號"的臨時順序節點,例如/shared_lock/192.168.0.1-R-000000001,那麼這個節點就表明了一個共享鎖。
獲取鎖:
1.客戶端調用create()方法建立一個相似於"/shared_lock/[hostname]-請求類型-序號"的臨時順序節點。
2.客戶端調用getChildren()接口來獲取全部已經建立的子節點列表。
3.肯定本身的節點序號在全部子節點中的順序
對於讀請求:
若是沒有比本身序號小的子節點,或是全部比本身序號小的 子節點都是讀請求,那麼代表已經成功獲取到了共享鎖,同時開始執行讀取邏輯。
若是比本身序號小的子節點中有寫請求,那麼就須要進入等待。向比本身序號小的最後一個寫請求節點註冊Watcher監聽。
對於寫請求:
若是本身不是序號最小的節點,那麼就須要進入等待。向比本身序號小的最後一個節點註冊Watcher監聽
4.等待Watcher通知,繼續進入步驟2
釋放鎖:
在定義鎖部分,咱們已經提到,/exclusive_lock/lock是一個臨時節點,所以在如下兩種狀況下,都有可能釋放鎖。
1.當前獲取鎖的客戶端發生宕機,那麼Zookeeper上的這個臨時節點就會被移除。
2.正常執行完業務邏輯以後,客戶端就會主動將本身建立的臨時節點刪除
mykit-lock
mykit架構中獨立出來的mykit-lock組件,旨在提供高併發架構下分佈式系統的分佈式鎖架構。
GitHub地址:github.com/sunshinelyz…
從PAXOS到ZOOKEEPER分佈式一致性原理和實踐
掘金小冊:Redis深度探險:核心原理與應用實踐