分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動作。如果不同的系統(tǒng)或是同一個系統(tǒng)的不同主機(jī)之間共享了一個或一組資源,那么訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分布式鎖。
公司主營業(yè)務(wù):網(wǎng)站設(shè)計制作、成都網(wǎng)站建設(shè)、移動網(wǎng)站開發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競爭能力。成都創(chuàng)新互聯(lián)公司是一支青春激揚、勤奮敬業(yè)、活力青春激揚、勤奮敬業(yè)、活力澎湃、和諧高效的團(tuán)隊。公司秉承以“開放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對我們的高要求,感謝他們從不同領(lǐng)域給我們帶來的挑戰(zhàn),讓我們激情的團(tuán)隊有機(jī)會用頭腦與智慧不斷的給客戶帶來驚喜。成都創(chuàng)新互聯(lián)公司推出寶應(yīng)免費做網(wǎng)站回饋大家。
為了保證一個方法或?qū)傩栽诟卟l(fā)情況下的同一時間只能被同一個線程執(zhí)行,在傳統(tǒng)單體應(yīng)用單機(jī)部署的情況下,可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLock或Synchronized)進(jìn)行互斥控制。在單機(jī)環(huán)境中,Java中提供了很多并發(fā)處理相關(guān)的API。但是,隨著業(yè)務(wù)發(fā)展的需要,原單體單機(jī)部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程、多進(jìn)程并且分布在不同機(jī)器上,這將使原單機(jī)部署情況下的并發(fā)控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力。為了解決這個問題就需要一種跨JVM的互斥機(jī)制來控制共享資源的訪問,這就是分布式鎖要解決的問題!
舉個例子:
機(jī)器A , 機(jī)器B是一個集群, A, B兩臺機(jī)器上的程序都是一樣的, 具備高可用性能.
A, B機(jī)器都有一個定時任務(wù), 每天晚上凌晨2點需要執(zhí)行一個定時任務(wù), 但是這個定時任務(wù)只能執(zhí)行一遍, 否則的話就會報錯, 那A,B兩臺機(jī)器在執(zhí)行的時候, 就需要搶鎖, 誰搶到鎖, 誰執(zhí)行, 誰搶不到, 就不用執(zhí)行了!
synchronize
分布式鎖控制分布式系統(tǒng)之間同步訪問資源的一種方式
分布式鎖是控制分布式系統(tǒng)之間同步同問共享資源的一種方式
在set命令中, 有很多選項可以用來修改命令的行為, 一下是set命令可用選項的基本語法
redis 127.0.0.1:6379>SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
- EX seconds 設(shè)置指定的到期時間(單位為秒)
- PX milliseconds 設(shè)置指定的到期時間(單位毫秒)
- NX: 僅在鍵不存在時設(shè)置鍵
- XX: 只有在鍵已存在時設(shè)置
方式1: 推介
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";
public static boolean getLock(JedisCluster jedisCluster, String lockKey, String requestId, int expireTime) {
// NX: 保證互斥性
String result = jedisCluster.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
方式2:
public static boolean getLock(String lockKey,String requestId,int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if(result == 1) {
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
注意: 推介方式1, 因為方式2中setnx和expire是兩個操作, 并不是一個原子操作, 如果setnx出現(xiàn)問題, 就是出現(xiàn)死鎖的情況, 所以推薦方式1
方式1: del命令實現(xiàn)
public static void releaseLock(String lockKey,String requestId) {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
方式2: redis+lua腳本實現(xiàn) 推薦
public static boolean releaseLock(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 (result.equals(1L)) {
return true;
}
return false;
}
理解了鎖的原理后,就會發(fā)現(xiàn),Zookeeper 天生就是一副分布式鎖的胚子。
首先,Zookeeper的每一個節(jié)點,都是一個天然的順序發(fā)號器。
在每一個節(jié)點下面創(chuàng)建子節(jié)點時,只要選擇的創(chuàng)建類型是有序(EPHEMERAL_SEQUENTIAL 臨時有序或者PERSISTENT_SEQUENTIAL 永久有序)類型,那么,新的子節(jié)點后面,會加上一個次序編號。這個次序編號,是上一個生成的次序編號加一
比如,創(chuàng)建一個用于發(fā)號的節(jié)點“/test/lock”,然后以他為父親節(jié)點,可以在這個父節(jié)點下面創(chuàng)建相同前綴的子節(jié)點,假定相同的前綴為“/test/lock/seq-”,在創(chuàng)建子節(jié)點時,同時指明是有序類型。如果是第一個創(chuàng)建的子節(jié)點,那么生成的子節(jié)點為/test/lock/seq-0000000000,下一個節(jié)點則為/test/lock/seq-0000000001,依次類推,等等。
其次,Zookeeper節(jié)點的遞增性,可以規(guī)定節(jié)點編號最小的那個獲得鎖。
一個zookeeper分布式鎖,首先需要創(chuàng)建一個父節(jié)點,盡量是持久節(jié)點(PERSISTENT類型),然后每個要獲得鎖的線程都會在這個節(jié)點下創(chuàng)建個臨時順序節(jié)點,由于序號的遞增性,可以規(guī)定排號最小的那個獲得鎖。所以,每個線程在嘗試占用鎖之前,首先判斷自己是排號是不是當(dāng)前最小,如果是,則獲取鎖。
第三,Zookeeper的節(jié)點監(jiān)聽機(jī)制,可以保障占有鎖的方式有序而且高效。
每個線程搶占鎖之前,先搶號創(chuàng)建自己的ZNode。同樣,釋放鎖的時候,就需要刪除搶號的Znode。搶號成功后,如果不是排號最小的節(jié)點,就處于等待通知的狀態(tài)。等誰的通知呢?不需要其他人,只需要等前一個Znode 的通知就可以了。當(dāng)前一個Znode 刪除的時候,就是輪到了自己占有鎖的時候。第一個通知第二個、第二個通知第三個,擊鼓傳花似的依次向后。
Zookeeper的節(jié)點監(jiān)聽機(jī)制,可以說能夠非常完美的,實現(xiàn)這種擊鼓傳花似的信息傳遞。具體的方法是,每一個等通知的Znode節(jié)點,只需要監(jiān)聽linsten或者 watch 監(jiān)視排號在自己前面那個,而且緊挨在自己前面的那個節(jié)點。 只要上一個節(jié)點被刪除了,就進(jìn)行再一次判斷,看看自己是不是序號最小的那個節(jié)點,如果是,則獲得鎖。
為什么說Zookeeper的節(jié)點監(jiān)聽機(jī)制,可以說是非常完美呢?
一條龍式的首尾相接,后面監(jiān)視前面,就不怕中間截斷嗎?比如,在分布式環(huán)境下,由于網(wǎng)絡(luò)的原因,或者服務(wù)器掛了或則其他的原因,如果前面的那個節(jié)點沒能被程序刪除成功,后面的節(jié)點不就永遠(yuǎn)等待么?
其實,Zookeeper的內(nèi)部機(jī)制,能保證后面的節(jié)點能夠正常的監(jiān)聽到刪除和獲得鎖。在創(chuàng)建取號節(jié)點的時候,盡量創(chuàng)建臨時znode 節(jié)點而不是永久znode 節(jié)點,一旦這個 znode 的客戶端與Zookeeper集群服務(wù)器失去聯(lián)系,這個臨時 znode 也將自動刪除。排在它后面的那個節(jié)點,也能收到刪除事件,從而獲得鎖。
說Zookeeper的節(jié)點監(jiān)聽機(jī)制,是非常完美的。還有一個原因。
Zookeeper這種首尾相接,后面監(jiān)聽前面的方式,可以避免羊群效應(yīng)。所謂羊群效應(yīng)就是每個節(jié)點掛掉,所有節(jié)點都去監(jiān)聽,然后做出反映,這樣會給服務(wù)器帶來巨大壓力,所以有了臨時順序節(jié)點,當(dāng)一個節(jié)點掛掉,只有它后面的那一個節(jié)點才做出反映。
###6.2 zookeeper實現(xiàn)分布式鎖的示例
zookeeper是通過臨時節(jié)點來實現(xiàn)分布式鎖.
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.junit.Before;
import org.junit.Test;
/**
* @ClassName ZookeeperLock
* @Description TODO
* @Author lingxiangxiang
* @Date 2:57 PM
* @Version 1.0
**/
public class ZookeeperLock {
// 定義共享資源
private static int NUMBER = 10;
private static void printNumber() {
// 業(yè)務(wù)邏輯: 秒殺
System.out.println("*********業(yè)務(wù)方法開始************\n");
System.out.println("當(dāng)前的值: " + NUMBER);
NUMBER--;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("*********業(yè)務(wù)方法結(jié)束************\n");
}
// 這里使用@Test會報錯
public static void main(String[] args) {
// 定義重試的側(cè)策略 1000 等待的時間(毫秒) 10 重試的次數(shù)
RetryPolicy policy = new ExponentialBackoffRetry(1000, 10);
// 定義zookeeper的客戶端
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("10.231.128.95:2181,10.231.128.96:2181,10.231.128.97:2181")
.retryPolicy(policy)
.build();
// 啟動客戶端
client.start();
// 在zookeeper中定義一把鎖
final InterProcessMutex lock = new InterProcessMutex(client, "/mylock");
//啟動是個線程
for (int i = 0; i <10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
// 請求得到的鎖
lock.acquire();
printNumber();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 釋放鎖, 還鎖
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
}
}
}
我們在討論使用分布式鎖的時候往往首先排除掉基于數(shù)據(jù)庫的方案,本能的會覺得這個方案不夠“高級”。從性能的角度考慮,基于數(shù)據(jù)庫的方案性能確實不夠優(yōu)異,整體性能對比:緩存 > Zookeeper、etcd > 數(shù)據(jù)庫。也有人提出基于數(shù)據(jù)庫的方案問題很多,不太可靠。數(shù)據(jù)庫的方案可能并不適合于頻繁寫入的操作.
下面我們來了解一下基于數(shù)據(jù)庫(MySQL)的方案,一般分為3類:基于表記錄、樂觀鎖和悲觀鎖。
要實現(xiàn)分布式鎖,最簡單的方式可能就是直接創(chuàng)建一張鎖表,然后通過操作該表中的數(shù)據(jù)來實現(xiàn)了。當(dāng)我們想要獲得鎖的時候,就可以在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。
為了更好的演示,我們先創(chuàng)建一張數(shù)據(jù)庫表,參考如下:
CREATE TABLE `database_lock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`resource` int NOT NULL COMMENT '鎖定的資源',
`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',
PRIMARY KEY (`id`),
UNIQUE KEY `uiq_idx_resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='數(shù)據(jù)庫分布式鎖表';
我們可以插入一條數(shù)據(jù):
INSERT INTO database_lock(resource, description) VALUES (1, 'lock');
因為表database_lock中resource是唯一索引, 所以其他請求提交到數(shù)據(jù)庫, 就會報錯, 并不會插入成功, 只有一個可以插入. 插入成功, 我們就獲取到鎖
INSERT INTO database_lock(resource, description) VALUES (1, 'lock');
這種實現(xiàn)方式非常的簡單,但是需要注意以下幾點:
這種鎖沒有失效時間,一旦釋放鎖的操作失敗就會導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中,其它線程無法獲得鎖。這個缺陷也很好解決,比如可以做一個定時任務(wù)去定時清理。
這種鎖的可靠性依賴于數(shù)據(jù)庫。建議設(shè)置備庫,避免單點,進(jìn)一步提高可靠性。
這種鎖是非阻塞的,因為插入數(shù)據(jù)失敗之后會直接報錯,想要獲得鎖就需要再次操作。如果需要阻塞式的,可以弄個for循環(huán)、while循環(huán)之類的,直至INSERT成功再返回。
這種鎖也是非可重入的,因為同一個線程在沒有釋放鎖之前無法再次獲得鎖,因為數(shù)據(jù)庫中已經(jīng)存在同一份記錄了。想要實現(xiàn)可重入鎖,可以在數(shù)據(jù)庫中添加一些字段,比如獲得鎖的主機(jī)信息、線程信息等,那么在再次獲得鎖的時候可以先查詢數(shù)據(jù),如果當(dāng)前的主機(jī)信息和線程信息等能被查到的話,可以直接把鎖分配給它。
顧名思義,系統(tǒng)認(rèn)為數(shù)據(jù)的更新在大多數(shù)情況下是不會產(chǎn)生沖突的,只在數(shù)據(jù)庫更新操作提交的時候才對數(shù)據(jù)作沖突檢測。如果檢測的結(jié)果出現(xiàn)了與預(yù)期數(shù)據(jù)不一致的情況,則返回失敗信息。
樂觀鎖大多數(shù)是基于數(shù)據(jù)版本(version)的記錄機(jī)制實現(xiàn)的。何謂數(shù)據(jù)版本號?即為數(shù)據(jù)增加一個版本標(biāo)識,在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表添加一個 “version”字段來實現(xiàn)讀取出數(shù)據(jù)時,將此版本號一同讀出,之后更新時,對此版本號加1。在更新過程中,會對版本號進(jìn)行比較,如果是一致的,沒有發(fā)生改變,則會成功執(zhí)行本次操作;如果版本號不一致,則會更新失敗。
為了更好的理解數(shù)據(jù)庫樂觀鎖在實際項目中的使用,這里也就舉了業(yè)界老生常談的庫存例子。一個電商平臺都會存在商品的庫存,當(dāng)用戶進(jìn)行購買的時候就會對庫存進(jìn)行操作(庫存減1代表已經(jīng)賣出了一件)。如果只是一個用戶進(jìn)行操作數(shù)據(jù)庫本身就能保證用戶操作的正確性,而在并發(fā)的情況下就會產(chǎn)生一些意想不到的問題:
?比如兩個用戶同時購買一件商品,在數(shù)據(jù)庫層面實際操作應(yīng)該是庫存進(jìn)行減2操作,但是由于高并發(fā)的情況,第一個用戶購買完成進(jìn)行數(shù)據(jù)讀取當(dāng)前庫存并進(jìn)行減1操作,由于這個操作沒有完全執(zhí)行完成。第二個用戶就進(jìn)入購買相同商品,此時查詢出的庫存可能是未減1操作的庫存導(dǎo)致了臟數(shù)據(jù)的出現(xiàn)【線程不安全操作】,通常如果是單JVM情況下使用JAVA內(nèi)置的鎖就能保證線程安全,如果在多JVM的情況下,使用分布式鎖也能實現(xiàn)【后期會補(bǔ)】,而本篇著重的去講數(shù)據(jù)庫層面的。
針對上面的問題,數(shù)據(jù)庫樂觀鎖也能保證線程安全,通常哎代碼層面我們都會這樣做:
select goods_num from goods where goods_name = "小本子";
update goods set goods_num = goods_num -1 where goods_name = "小本子";
上面的SQL是一組的,通常先查詢出當(dāng)前的goods_num,然后再goods_num上進(jìn)行減1的操作修改庫存,當(dāng)并發(fā)的情況下,這條語句可能導(dǎo)致原本庫存為3的一個商品經(jīng)過兩個人購買還剩下2庫存的情況就會導(dǎo)致商品的多賣。那么數(shù)據(jù)庫樂觀鎖是如何實現(xiàn)的呢?
首先定義一個version字段用來當(dāng)作一個版本號,每次的操作就會變成這樣:
select goods_num,version from goods where goods_name = "小本子";
update goods set goods_num = goods_num -1,version =查詢的version值自增 where goods_name ="小本子" and version=查詢出來的version;
其實,借助更新時間戳(updated_at)也可以實現(xiàn)樂觀鎖,和采用version字段的方式相似:更新操作執(zhí)行前線獲取記錄當(dāng)前的更新時間,在提交更新時,檢測當(dāng)前更新時間是否與更新開始時獲取的更新時間戳相等。
除了可以通過增刪操作數(shù)據(jù)庫表中的記錄以外,我們還可以借助數(shù)據(jù)庫中自帶的鎖來實現(xiàn)分布式鎖。在查詢語句后面增加FOR UPDATE,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加悲觀鎖,也稱排他鎖。當(dāng)某條記錄被加上悲觀鎖之后,其它線程也就無法再改行上增加悲觀鎖。
悲觀鎖,與樂觀鎖相反,總是假設(shè)最壞的情況,它認(rèn)為數(shù)據(jù)的更新在大多數(shù)情況下是會產(chǎn)生沖突的。
在使用悲觀鎖的同時,我們需要注意一下鎖的級別。MySQL InnoDB引起在加鎖的時候,只有明確地指定主鍵(或索引)的才會執(zhí)行行鎖 (只鎖住被選取的數(shù)據(jù)),否則MySQL 將會執(zhí)行表鎖(將整個數(shù)據(jù)表單給鎖住)。
在使用悲觀鎖時,我們必須關(guān)閉MySQL數(shù)據(jù)庫的自動提交屬性(參考下面的示例),因為MySQL默認(rèn)使用autocommit模式,也就是說,當(dāng)你執(zhí)行一個更新操作后,MySQL會立刻將結(jié)果進(jìn)行提交。
mysql> SET AUTOCOMMIT = 0;
Query OK, 0 rows affected (0.00 sec)
這樣在使用FOR UPDATE獲得鎖之后可以執(zhí)行相應(yīng)的業(yè)務(wù)邏輯,執(zhí)行完之后再使用COMMIT來釋放鎖。
我們不妨沿用前面的database_lock表來具體表述一下用法。假設(shè)有一線程A需要獲得鎖并執(zhí)行相應(yīng)的操作,那么它的具體步驟如下:
STEP1 - 獲取鎖:SELECT * FROM database_lock WHERE id = 1 FOR UPDATE;。
STEP2 - 執(zhí)行業(yè)務(wù)邏輯。
STEP3 - 釋放鎖:COMMIT
分享名稱:分布式鎖介紹
網(wǎng)站URL:http://muchs.cn/article6/pdgeig.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供虛擬主機(jī)、網(wǎng)站排名、微信小程序、網(wǎng)站導(dǎo)航、網(wǎng)頁設(shè)計公司、做網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)