轉載:http://www.cnblogs.com/0201zcr/p/5942748.html
轉載: http://blog.csdn.net/fengshizty/article/details/53561562
轉載:http://www.hollischuang.com/archives/1716
轉載:http://www.cnblogs.com/zhongkaiuu/p/redisson.html
轉載:https://yq.aliyun.com/articles/60663
轉載:http://blog.csdn.net/josn_hao/article/details/78412694 一 分布式鎖
由於在平時的工作中,線上伺服器是分布式多台部署的,經常會面臨解決分布式情境下資料一致性的問題,那麼就要利用分布式鎖來解決這些問題。
分布式的CAP理論告訴我們“任何一個分布式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取捨。在互連網領域的絕大多數的情境中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證“最終一致性”,只要這個最終時間是在使用者可以接受的範圍內即可。
1. 樂觀鎖和悲觀鎖
悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號碼等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高輸送量,像資料庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖
2. 分布式鎖的一般實現方式
針對分布式鎖的實現,目前比較常用的有以下幾種方案:
基於資料庫實現分布式鎖 基於緩衝(redis,memcached,tair)實現分布式鎖 基於Zookeeper實現分布式鎖
1 基於資料庫實現分布式鎖
基於資料庫實現的分布式鎖分為行鎖(for update實現的record lock)和獨佔鎖定/表鎖(添加唯一標識子段)的方式實現。
實現原理
要實現分布式鎖,最簡單的方式可能就是直接建立一張鎖表,然後通過操作該表中的資料來實現了。當我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。建立這樣一張資料庫表:
CREATE TABLE `methodLock` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名', `desc` varchar(1024) NOT NULL DEFAULT '備忘資訊', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '儲存資料時間,自動產生', PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
當我們想要鎖住某個方法時,執行以下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
因為我們對method_name做了唯一性限制式,這裡如果有多個請求同時提交到資料庫的話,資料庫會保證只有一個操作可以成功,那麼我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。當方法執行完畢之後,想要釋放鎖的話,需要執行以下Sql:
delete from methodLock where method_name ='method_name'
除了可以通過增刪操作資料表中的記錄以外,其實還可以藉助資料中內建的鎖來實現分布式的鎖。可以通過資料庫的獨佔鎖定來實現分布式鎖。 基於MySql的InnoDB引擎,可以使用以下方法來實現加鎖操作:、
public boolean lock(){ connection.setAutoCommit(false) while(true){ try{ result = select * from methodLock where method_name=xxx for update; if(result==null){ return true; } }catch(Exception e){ } sleep(1000); } return false;}
在查詢語句後面增加for update,資料庫會在查詢過程中給資料庫表增加
獨佔鎖定(這裡再多提一句,InnoDB引擎在加鎖的時候,只有
通過索引進行檢索的時候才會使用行級鎖,否則會使用
表級鎖。這裡我們希望使用行級鎖,就要給method_name添加索引,值得注意的是,這個索引一定要建立成
唯一索引,否則會出現多個重載方法之間無法同時被訪問的問題。重載方法的話建議把參數類型也加上。)當某條記錄被加上獨佔鎖定之後,其他線程無法再在該行記錄上增加獨佔鎖定。
執行完方法之後,再通過以下方法解鎖:
public void unlock(){ connection.commit();}
優點
直接藉助資料庫,容易理解。
缺點
會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越複雜。
操作資料庫需要一定的開銷,效能問題需要考慮。(sql逾時異常的問題{架構層的事務逾時/jdbc的查詢逾時/Socket的讀逾時})
使用資料庫的行級鎖並不一定靠譜,尤其是當我們的鎖表並不大的時候。 2 基於緩衝實現分布式鎖
memcached鎖:
實現原理
memcached帶有add函數,利用add函數的特性即可實現分布式鎖。add和set的區別在於:如果多線程並發set,則每個set都會成功,但最後儲存的值以最後的set的線程為準。而add的話則相反,add會添加第一個到達的值,並返回true,後續的添加則都會返回false。利用該點即可很輕鬆地實現分布式鎖。
優點
並發高效。
缺點
(1)memcached採用列入LRU置換策略,所以如果記憶體不夠,可能導致緩衝中的鎖資訊丟失。
(2)memcached無法持久化,一旦重啟,將導致資訊丟失。
(3)通過逾時時間來控制鎖的失效時間並不是十分的靠譜。
3 基於zookeeper實現分布式鎖 實現原理:
基於zookeeper瞬時有序節點實現的分布式鎖,大致思想即為:每個用戶端對某個功能加鎖時,在zookeeper上的與該功能對應的指定節點的目錄下,產生一個唯一的瞬時有序節點。判斷是否擷取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死結問題。
優點
鎖安全性高,zk可持久化
缺點
效能開銷比較高。因為其需要動態產生、銷毀瞬時節點來實現鎖功能。
實現
可以直接採用zookeeper第三方庫curator即可方便地實現分布式鎖。以下為基於curator實現的zk分布式鎖核心代碼:
3. 分布式鎖的適用情境舉例
情境一: 比如分配任務情境。在這個情境中,由於是公司的業務後台系統,主要是用於審核人員的審核工作,並發量並不是很高,而且任務的分配規則設計成了通過審核人員每次主動的請求拉取,然後服務端從任務池中隨機的選取任務進行分配。這個情境看到這裡你會覺得比較單一,但是實際的分配過程中,由於涉及到了按使用者聚類的問題,所以要比我描述的複雜,但是這裡為了說明問題,大家可以把問題簡單化理解。那麼在使用過程中,主要是為了避免同一個任務同時被兩個審核人員擷取到的問題。我最終使用了基於資料庫資源表的分布式鎖來解決的問題。
情境二: 比如支付情境。在這個情境中,我提供給使用者三個用於保護使用者隱私的手機號碼(這些號碼是從電訊廠商處擷取的,和真實手機號碼看起來是一樣的),讓使用者選擇其中一個進行購買,使用者購買付款後,我需要將使用者選擇的號碼分配給使用者使用,同時也要將沒有選擇的釋放掉。在這個過程中,給使用者篩選的號碼要在一定時間內(使用者篩選正常時間範圍內)讓目前使用者對這個產品具有獨佔性,以便保證付款後是100%可以拿到;同時由於產品資源集區的資源有限,還要保持資源的流動性,即不能讓資源長時間被某個使用者佔用著。對於服務的設計目標,一期項目上線的時候至少能夠支援峰值qps為300的請求,同時在設計的過程中要考慮到使用者體驗的問題。我最終使用了memecahed的add()方法和基於資料庫資源表的分布式鎖來解決的問題。
情境三: 我有一個資料服務,每天調用量在3億,每天按86400秒計算的qps在4000左右,由於服務的白天調用量要明顯高於晚上,所以白天下午的峰值qps達到6000的,一共有4台伺服器,單台qps要能達到3000以上。我最終使用了redis的setnx()和expire()的分布式鎖解決的問題。
情境四:情境一和情境二的升級版。在這個情境中,不涉及支付。但是由於資源分派一次過程中,需要保持涉及一致性的地方增加,而且一期的設計目標要達到峰值qps500,所以需要我們對情境進一步的最佳化。我最終使用了redis的setnx()、expire()和基於資料庫表的分布式鎖來解決的問題。 轉載:http://www.cnblogs.com/PurpleDream/p/5559352.html 二 Redis分布式鎖的實現原理:setnx/getset
1)setNX(SET if Not eXists)
文法:SETNX key value
SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫,其操作為:將 key 的值設為 value ,若且唯若 key 不存在。若給定的 key 已經存在,則 SETNX 不做任何動作。
傳回值:
設定成功,返回 1 。
設定失敗,返回 0 。
所以我們使用執行下面的命令:
SETNX lock.foo <current Unix time + lock timeout + 1>
如返回1,則該用戶端獲得鎖,把lock.foo的索引值設定為時間值表示該鍵已被鎖定,該用戶端最後可以通過DEL lock.foo來釋放該鎖。
如返回0,表明該鎖已被其他用戶端取得,這時我們可以先返回或進行重試等對方完成或等待鎖逾時。
2)getSET
文法:GETSET key value
將給定 key 的值設為 value ,並返回 key 的舊值(old value)。當 key 存在但不是字串類型時,返回一個錯誤。
傳回值:
返回給定 key 的舊值。
當 key 沒有舊值時,也即是, key 不存在時,返回 nil 。
3)get
文法:GET key
傳回值:
當 key 不存在時,返回 nil ,否則,返回 key 的值。
如果 key 不是字串類型,那麼返回一個錯誤
三 Redis分布式鎖基本方式
redis通常可以使用setnx來實現分布式鎖。
1. 擷取鎖
public static void lock(Jedis jedis, String lockKey, String requestId, int expireTime) { Long result = jedis.setnx(lockKey, requestId); if (result == 1) { // 若在這裡程式突然崩潰,則無法設定到期時間,將發生死結 jedis.expire(lockKey, expireTime); }}
2. 釋放鎖
public static void releaselock1(Jedis jedis, String lockKey) { jedis.del(lockKey);}
setnx來建立一個key,如果key不存在則建立成功返回1,如果key已經存在則返回0。依照上述來判定是否擷取到了鎖。擷取到鎖的執行商務邏輯,完畢後刪除lock_key,來實現釋放鎖,其他未擷取到鎖的則進行不斷重試,直到自己擷取到了鎖。
上述邏輯在正常情況下是OK的,但是一旦擷取到鎖的用戶端掛了,沒有執行上述釋放鎖的操作,則其他用戶端就無法擷取到鎖了。
四 Redis分布式鎖的實現
簡單方式實現的分布式鎖在用戶端掉線時無法釋放資源,所以在這種情況下有2種方式來解決:
為lock_key設定一個到期時間 對lock_key的value進行判斷是否到期 以第一種為例,在set索引值的時候帶上到期時間,即使掛了,也會在到期時間之後,其他用戶端能夠重新競爭擷取鎖。
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 嘗試擷取分布式鎖 * @param jedis Redis用戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @param expireTime 超期時間 * @return 是否擷取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; }}
可以看到,我們加鎖就一行代碼:jedis.set(String key, String value, String nxxx, String expx, int time),這個set()方法一共有五個形參:
第一個為key,我們使用key來當鎖,因為key是唯一的。
第二個為value,我們傳的是requestId,很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什麼還要用到value。原因就是我們在上面講到可靠性時,分布式鎖要滿足第四個條件解鈴還須繫鈴人,通過給value賦值為requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據。requestId可以使用
UUID.randomUUID().toString()方法產生。
第三個為nxxx,這個參數我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作;
第四個為expx,這個參數我們傳的是PX,意思是我們要給這個key加一個到期的設定,具體時間由第五個參數決定。
第五個為time,與第四個參數相呼應,代表key的到期時間。
總的來說,執行上面的set()方法就只會導致兩種結果:1. 當前沒有鎖(key不存在),那麼就進行
加鎖操作,並對鎖設定個
有效期間,同時value表示加鎖的用戶端。2. 已有鎖存在,不做任何操作。
以第二種為例,一旦發現lock_key的值已經小於目前時間了,說明該key到期了,然後對該key進行getset設定,一旦getset傳回值是原來的到期值,說明當前用戶端是第一個來操作的,代表擷取到了鎖,一旦getset傳回值不是原來到期時間則說明前面已經有人修改了,則代表沒有擷取到鎖,詳細見用Redis實現分布式鎖,改正如下:
# get locklock = 0while lock != 1: timestamp = current_unix_time + lock_timeout lock = SETNX lock.foo timestamp if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)): break; else: sleep(10ms)# do your jobdo_job()# releaseif now() < GET lock.foo: DEL lock.foo
lock timeout的存在也使得失去了鎖的意義,即存在並發的現象。一旦出現鎖的租約時間,就意味著擷取到鎖的用戶端
必須在租約之內執行完畢商務邏輯,一旦商務邏輯執行時間過長,租約到期,就會引發
並發問題。所以有lock timeout的可靠性並不是那麼的高。
對於請求鎖的用戶端而言,如何才能知道鎖被釋放了呢。實現方式一般有2種情況:
1 沒有擷取到鎖的用戶端不斷嘗試擷取鎖
2 伺服器端通知用戶端鎖被釋放了
當然第二種情況是最優的(用戶端所做的無用功最少),如ZooKeeper通過註冊watcher來得到鎖釋放的通知。而資料庫、redis沒有辦法來通知用戶端鎖釋放了,那用戶端就只能傻傻的不斷嘗試擷取鎖了。
鎖的刪除;
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 釋放分布式鎖 * @param jedis Redis用戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @return 是否釋放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }}
第一行代碼,我們寫了一個簡單的Lua指令碼代碼。 首先擷取鎖對應的
value值,檢查是否與
requestId相等,如果相等則刪除鎖(解鎖)。第二行代碼,我們將Lua代碼傳到jedis.eval()方法裡,並使參數KEYS[1]賦值為
lockKey,ARGV[1]賦值為
requestId。eval()方法是將Lua代碼交給Redis服務端執行。
五 RedLock
RedLock是Redis官方給出的分布式鎖原理。官方文檔描述:”有很多三方庫和文章描述如何用Redis實現一個分布式鎖管理器,但是這些庫實現的方式差別很大,而且很多簡單的實現其實只需採用稍微增加一點複雜的設計就可以獲得更好的可靠性。 這篇文章的目的就是嘗試提出一種官方權威的用Redis實現分布式鎖管理器的演算法,我們把這個演算法稱為RedLock,我們相信這個演算法會比一般的普通方法更加安全可靠。“
為什麼基於故障切換的方案不夠好
為了理解我們想要提高的到底是什麼,我們先看下當前大多數基於Redis的分布式鎖三方庫的現狀。 用Redis來實現分布式鎖最簡單的方式就是在執行個體裡建立一個索引值,建立出來的索引值一般都是有一個逾時時間的(這個是Redis內建的逾時特性),所以每個鎖最終都會釋放。而當一個用戶端想要釋放鎖時,它只需要刪除這個索引值即可。 表面來看,這個方法似乎很管用,但是這裡存在一個問題:
在我們的系統架構裡存在一個單點故障,如果Redis的master節點宕機了怎麼辦呢。
有人可能會說:加一個slave節點。在master宕機時用slave就行了。但是其實這個方案明顯是不可行的,因為這種方案無法保證第1個安全互斥屬性,因為Redis的複製是非同步。 總的來說,這個方案裡有一個明顯的競爭條件(race condition),舉例來說:
用戶端A在master節點拿到了鎖。 master節點在把A建立的key寫入slave之前宕機了。 slave變成了master節點 B也得到了和A還持有的相同的鎖(因為原來的slave裡還沒有A持有鎖的資訊) 當然,在某些特殊情境下,前面提到的這個方案則完全沒有問題,比如 在宕機期間,多個用戶端允許同時都持有鎖,如果你可以容忍這個問題的話,那用這個基於複製的方案就完全沒有問題,否則的話我們還是建議你採用這篇文章裡接下來要描述的方案。
Redlock演算法
在分布式版本的演算法裡我們假設我們有N個Redis master節點,這些節點都是完全獨立的,我們不用任何複製或者其他隱含的分布式協調演算法。我們已經描述了如何在單節點環境下安全地擷取和釋放鎖。因此我們理所當然地應當用這個方法在每個單節點裡來擷取和釋放鎖。在我們的例子裡面我們把N設成5,這個數字是一個相對比較合理的數值,因此我們需要在不同的電腦或者虛擬機器上運行5個master節點來保證他們大多數情況下都不會同時宕機。一個用戶端需要做如下操作來擷取鎖:
1.擷取目前時間(單位是毫秒)。
2.輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裡,用戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的逾時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節點鎖請求的逾時時間可能是5-50毫秒的範圍,這個可以防止一個用戶端在某個宕掉的master節點上阻塞過長時間,如果一個master節點不可用了,我們應該儘快嘗試下一個master節點。
3.用戶端計算第二步中擷取鎖所花的時間,只有當用戶端在大多數master節點上成功擷取了鎖(在這裡是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認為是擷取成功了。
4.如果鎖擷取成功了,那現在鎖自動釋放時間就是最初的鎖釋放時間減去之前擷取鎖所消耗的時間。
5.如果鎖擷取失敗了,不管是因為擷取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,用戶端都會到每個master節點上釋放鎖,即便是那些他認為沒有擷取成功的鎖。
RedLock實現
redisson是redis官網推薦的java語言實現分布式鎖的項目。Redisson在基於NIO的Netty架構上,充分的利用了Redis索引值資料庫提供的一系列優勢,在Java工具 + 生產力包中常用介面的基礎上,為使用者提供了一系列具有分布式特性的常用工具類。Redisson提供了分布式對象/分布式集和/分布式鎖和分布式服務。
redisson支援4種連結redis的方式:
Cluster(叢集) Sentinel servers(哨兵) Master/Slave servers(主從) Single server(單機)
使用redisson實現分布式鎖可以通過簡單的配置和使用兩部分完成:
1、RedissonManager類,管理redisson的初始化等操作。
public class RedissonManager { private static final String RAtomicName = "genId_"; private static Config config = new Config(); private static Redisson redisson = null; public static void init(){ try { config.useClusterServers() //這是用的叢集server .setScanInterval(2000) //設定叢集狀態掃描時間 .setMasterConnectionPoolSize(10000) //設定串連數 .setSlaveConnectionPoolSize(10000) .addNodeAddress("127.0.0.1:6379","127.0.0.1:6380"); redisson = Redisson.create(config); //清空自增的ID數字 RAtomicLong atomicLong = redisson.getAtomicLong(RAtomicName); atomicLong.set(1); }catch (Exception e){ e.printStackTrace(); } } public static Redisson getRedisson(){ return redisson; } /** 擷取redis中的原子ID */ public static Long nextID(){ RAtomicLong atomicLong = getRedisson().getAtomicLong(RAtomicName); atomicLong.incrementAndGet(); return atomicLong.get(); }}
2. DistributedRedisLock類,提供鎖和解鎖方法
public class DistributedRedisLock { private static Redisson redisson = RedissonManager.getRedisson(); private static final String LOCK_TITLE = "redisLock_"; public static void acquire(String lockName){ String key = LOCK_TITLE + lockName; RLock mylock = redisson.getLock(key); mylock.lock(2, TimeUnit.MINUTES); //lock提供帶timeout參數,timeout結束強制解鎖,防止死結 System.err.println("======lock======"+Thread.currentThread().getName()); } public static void release(String lockName){ String key = LOCK_TITLE + lockName; RLock mylock = redisson.getLock(key); mylock.unlock(); System.err.println("======unlock======"+Thread.currentThread().getName()); }}
3. 測試
private static void redisLock(){ RedissonManager.init(); //初始化 for (int i = 0; i < 100; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { try { String key = "test123"; DistributedRedisLock.acquire(key); Thread.sleep(1000); //獲得鎖之後可以進行相應的處理 System.err.println("======獲得鎖後進行相應的操作======"); DistributedRedisLock.release(key); System.err.println("============================="); } catch (Exception e) { e.printStackTrace(); } } }); t.start(); } }
測試結果:
======lock======Thread-91======獲得鎖後進行相應的操作============unlock======Thread-91===================================lock======Thread-63======獲得鎖後進行相應的操作============unlock======Thread-63===================================lock======Thread-31======獲得鎖後進行相應的操作============unlock======Thread-31===================================lock======Thread-97======獲得鎖後進行相應的操作============unlock======Thread-97===================================lock======Thread-8======獲得鎖後進行相應的操作============unlock======Thread-8=============================