自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

為什么Zookeeper天生就是一副分布式鎖的胚子?

運維 數(shù)據(jù)庫運維 分布式
什么是分布式鎖?分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。在分布式系統(tǒng)中,常常需要協(xié)調他們的動作。

什么是分布式鎖?分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。在分布式系統(tǒng)中,常常需要協(xié)調他們的動作。

[[317600]]

圖片來自 Pexels

如果不同的系統(tǒng)或是同一個系統(tǒng)的不同主機之間共享了一個或一組資源,那么訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分布式鎖。

為什么要使用分布式鎖

為了保證一個方法或屬性在高并發(fā)情況下的同一時間只能被同一個線程執(zhí)行。

在傳統(tǒng)單體應用單機部署的情況下,可以使用 Java 并發(fā)處理相關的 API(如 ReentrantLock 或 Synchronized)進行互斥控制;在單機環(huán)境中,Java 中提供了很多并發(fā)處理相關的 API。

但是,隨著業(yè)務發(fā)展的需要,原單體單機部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程、多進程并且分布在不同機器上,這將使原單機部署情況下的并發(fā)控制鎖策略失效,單純的 Java API 并不能提供分布式鎖的能力。

為了解決這個問題就需要一種跨 JVM 的互斥機制來控制共享資源的訪問,這就是分布式鎖要解決的問題!

舉個例子:機器 A,機器 B 是一個集群。A,B 兩臺機器上的程序都是一樣的,具備高可用性能。

A,B 機器都有一個定時任務,每天晚上凌晨 2 點需要執(zhí)行一個定時任務,但是這個定時任務只能執(zhí)行一遍,否則的話就會報錯。

那 A,B 兩臺機器在執(zhí)行的時候,就需要搶鎖,誰搶到鎖,誰執(zhí)行,誰搶不到,就不用執(zhí)行了!

鎖的處理

鎖的處理方式如下:

  • 單個應用中使用鎖:(單進程多線程)Synchronize。
  • 分布式鎖控制分布式系統(tǒng)之間同步訪問資源的一種方式。
  • 分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。

分布式鎖的實現(xiàn)

分布式鎖的實現(xiàn)方式如下:

  • 基于數(shù)據(jù)的樂觀鎖實現(xiàn)分布式鎖
  • 基于 Zookeeper 臨時節(jié)點的分布式鎖
  • 基于 Redis 的分布式鎖

Redis 的分布式鎖

獲取鎖

在 set 命令中,有很多選項可以用來修改命令的行為,以下是 set 命令可用選項的基本語法:

  1. redis 127.0.0.1:6379>SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX] 
  2.  
  3. - EX seconds 設置指定的到期時間(單位為秒) 
  4. - PX milliseconds 設置指定的到期時間(單位毫秒) 
  5. - NX: 僅在鍵不存在時設置鍵 
  6. - XX: 只有在鍵已存在時設置 

方式 1:推介

  1. private static final String LOCK_SUCCESS = "OK"
  2.   private static final String SET_IF_NOT_EXIST = "NX"
  3.   private static final String SET_WITH_EXPIRE_TIME = "PX"
  4.  
  5. ublic static boolean getLock(JedisCluster jedisCluster, String lockKey, String requestId, int expireTime) { 
  6.       // NX: 保證互斥性 
  7.       String result = jedisCluster.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); 
  8.       if (LOCK_SUCCESS.equals(result)) { 
  9.           return true
  10.      } 
  11.       return false
  12.  } 

方式 2:

  1. public static boolean getLock(String lockKey,String requestId,int expireTime) { 
  2.     Long result = jedis.setnx(lockKey, requestId); 
  3.     if(result == 1) { 
  4.         jedis.expire(lockKey, expireTime); 
  5.         return true
  6.     } 
  7.     return false

注意:推介方式 1,因為方式 2 中 setnx 和 expire 是兩個操作,并不是一個原子操作,如果 setnx 出現(xiàn)問題,就是出現(xiàn)死鎖的情況,所以推薦方式 1。

釋放鎖

方式 1:del 命令實現(xiàn)

  1. public static void releaseLock(String lockKey,String requestId) { 
  2.    if (requestId.equals(jedis.get(lockKey))) { 
  3.        jedis.del(lockKey); 
  4.   } 

方式 2:Redis+Lua 腳本實現(xiàn)(推薦)

  1. public static boolean releaseLock(String lockKey, String requestId) { 
  2.        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return 
  3. redis.call('del', KEYS[1]) else return 0 end"; 
  4.        Object result = jedis.eval(script, Collections.singletonList(lockKey), 
  5. Collections.singletonList(requestId)); 
  6.        if (result.equals(1L)) { 
  7.            return true
  8.        return false
  9.   } 

Zookeeper 的分布式鎖

Zookeeper 分布式鎖實現(xiàn)原理

理解了鎖的原理后,就會發(fā)現(xiàn),Zookeeper 天生就是一副分布式鎖的胚子。

首先,Zookeeper 的每一個節(jié)點,都是一個天然的順序發(fā)號器。

在每一個節(jié)點下面創(chuàng)建子節(jié)點時,只要選擇的創(chuàng)建類型是有序(EPHEMERAL_SEQUENTIAL 臨時有序或者 PERSISTENT_SEQUENTIAL 永久有序)類型,那么,新的子節(jié)點后面,會加上一個次序編號。

這個次序編號,是上一個生成的次序編號加 1,比如,創(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ī)定排號最小的那個獲得鎖。

所以,每個線程在嘗試占用鎖之前,首先判斷自己是排號是不是當前最小,如果是,則獲取鎖。

第三,Zookeeper 的節(jié)點監(jiān)聽機制,可以保障占有鎖的方式有序而且高效。

每個線程搶占鎖之前,先搶號創(chuàng)建自己的 ZNode。同樣,釋放鎖的時候,就需要刪除搶號的 Znode。

搶號成功后,如果不是排號最小的節(jié)點,就處于等待通知的狀態(tài)。等誰的通知呢?不需要其他人,只需要等前一個 Znode 的通知就可以了。

當前一個 Znode 刪除的時候,就是輪到了自己占有鎖的時候。第一個通知第二個、第二個通知第三個,擊鼓傳花似的依次向后。

Zookeeper 的節(jié)點監(jiān)聽機制,可以說能夠非常完美的,實現(xiàn)這種擊鼓傳花似的信息傳遞。

具體的方法是,每一個等通知的 Znode 節(jié)點,只需要監(jiān)聽 linsten 或者 watch 監(jiān)視排號在自己前面那個,而且緊挨在自己前面的那個節(jié)點。

只要上一個節(jié)點被刪除了,就進行再一次判斷,看看自己是不是序號最小的那個節(jié)點,如果是,則獲得鎖。

為什么說 Zookeeper 的節(jié)點監(jiān)聽機制,可以說是非常完美呢?

一條龍式的首尾相接,后面監(jiān)視前面,就不怕中間截斷嗎?比如,在分布式環(huán)境下,由于網(wǎng)絡的原因,或者服務器掛了或者其他的原因,如果前面的那個節(jié)點沒能被程序刪除成功,后面的節(jié)點不就永遠等待么?

其實,Zookeeper 的內(nèi)部機制,能保證后面的節(jié)點能夠正常的監(jiān)聽到刪除和獲得鎖。

在創(chuàng)建取號節(jié)點的時候,盡量創(chuàng)建臨時 Znode 節(jié)點而不是永久 Znode 節(jié)點。

一旦這個 Znode 的客戶端與 Zookeeper 集群服務器失去聯(lián)系,這個臨時 Znode 也將自動刪除。排在它后面的那個節(jié)點,也能收到刪除事件,從而獲得鎖。

說 Zookeeper 的節(jié)點監(jiān)聽機制,是非常完美的。還有一個原因。Zookeeper 這種首尾相接,后面監(jiān)聽前面的方式,可以避免羊群效應。

所謂羊群效應就是每個節(jié)點掛掉,所有節(jié)點都去監(jiān)聽,然后做出反映,這樣會給服務器帶來巨大壓力,所以有了臨時順序節(jié)點,當一個節(jié)點掛掉,只有它后面的那一個節(jié)點才做出反映。

Zookeeper 分布式鎖實現(xiàn)示例

Zookeeper 是通過臨時節(jié)點來實現(xiàn)分布式鎖:

  1. terruptedException e) { 
  2.            e.printStackTrace(); 
  3.       } 
  4.        System.out.println("*********業(yè)務方法結束************\n"); 
  5.  
  6.   } 
  7.  
  8.    // 這里使用@Test會報錯 
  9.    public static void main(String[] args) { 
  10.        // 定義重試的側策略 1000 等待的時間(毫秒) 10 重試的次數(shù) 
  11.        RetryPolicy policy = new ExponentialBackoffRetry(1000, 10); 
  12.  
  13.        // 定義zookeeper的客戶端 
  14.        CuratorFramework client = CuratorFrameworkFactory.builder() 
  15.               .connectString("10.231.128.95:2181,10.231.128.96:2181,10.231.128.97:2181"
  16.               .retryPolicy(policy) 
  17.               .build(); 
  18.        // 啟動客戶端 
  19.        client.start(); 
  20.  
  21.        // 在zookeeper中定義一把鎖 
  22.        final InterProcessMutex lock = new InterProcessMutex(client, "/mylock"); 
  23.  
  24.        //啟動是個線程 
  25.        for (int i = 0; i <10; i++) { 
  26.            new Thread(new Runnable() { 
  27.                @Override 
  28.                public void run() { 
  29.                    try { 
  30.                        // 請求得到的鎖 
  31.                        lock.acquire(); 
  32.                        printNumber(); 
  33.                   } catch (Exception e) { 
  34.                        e.printStackTrace(); 
  35.                   } finally { 
  36.                        // 釋放鎖 
  37.                        try { 
  38.                            lock.release(); 
  39.                       } catch (Exception e) { 
  40.                            e.printStackTrace(); 
  41.                       } 
  42.                   } 
  43.               } 
  44.           }).start(); 
  45.       } 
  46.  
  47.   } 

基于數(shù)據(jù)的分布式鎖

我們在討論使用分布式鎖的時候往往首先排除掉基于數(shù)據(jù)庫的方案,本能的會覺得這個方案不夠“高級”。

從性能的角度考慮,基于數(shù)據(jù)庫的方案性能確實不夠優(yōu)異,整體性能對比:緩存>Zookeeper、etcd>數(shù)據(jù)庫。

也有人提出基于數(shù)據(jù)庫的方案問題很多,不太可靠。數(shù)據(jù)庫的方案可能并不適合于頻繁寫入的操作。

下面我們來了解一下基于數(shù)據(jù)庫(MySQL)的方案,一般分為三類:

  • 基于表記錄
  • 樂觀鎖
  • 悲觀鎖

基于表記錄

要實現(xiàn)分布式鎖,最簡單的方式可能就是直接創(chuàng)建一張鎖表,然后通過操作該表中的數(shù)據(jù)來實現(xiàn)了。

當我們想要獲得鎖的時候,就可以在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。

為了更好的演示,我們先創(chuàng)建一張數(shù)據(jù)庫表,參考如下:

  1. CREATE TABLE `database_lock` ( 
  2. `id` BIGINT NOT NULL AUTO_INCREMENT, 
  3. `resource` int NOT NULL COMMENT '鎖定的資源'
  4. `description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述'
  5. PRIMARY KEY (`id`), 
  6. UNIQUE KEY `uiq_idx_resource` (`resource`) 
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='數(shù)據(jù)庫分布式鎖表'

①獲得鎖

我們可以插入一條數(shù)據(jù):

  1. INSERT INTO database_lock(resource, description) VALUES (1, 'lock'); 

因為表 database_lock 中 resource 是唯一索引,所以其他請求提交到數(shù)據(jù)庫,就會報錯,并不會插入成功,只有一個可以插入。插入成功,我們就獲取到鎖。

②刪除鎖

  1. INSERT INTO database_lock(resource, description) VALUES (1, 'lock'); 

這種實現(xiàn)方式非常的簡單,但是需要注意以下幾點:

①這種鎖沒有失效時間,一旦釋放鎖的操作失敗就會導致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法獲得鎖。這個缺陷也很好解決,比如可以做一個定時任務去定時清理。

②這種鎖的可靠性依賴于數(shù)據(jù)庫。建議設置備庫,避免單點,進一步提高可靠性。

③這種鎖是非阻塞的,因為插入數(shù)據(jù)失敗之后會直接報錯,想要獲得鎖就需要再次操作。

如果需要阻塞式的,可以弄個 for 循環(huán)、while 循環(huán)之類的,直至 INSERT 成功再返回。

④這種鎖也是非可重入的,因為同一個線程在沒有釋放鎖之前無法再次獲得鎖,因為數(shù)據(jù)庫中已經(jīng)存在同一份記錄了。

想要實現(xiàn)可重入鎖,可以在數(shù)據(jù)庫中添加一些字段,比如獲得鎖的主機信息、線程信息等。

那么在再次獲得鎖的時候可以先查詢數(shù)據(jù),如果當前的主機信息和線程信息等能被查到的話,可以直接把鎖分配給它。

樂觀鎖

顧名思義,系統(tǒng)認為數(shù)據(jù)的更新在大多數(shù)情況下是不會產(chǎn)生沖突的,只在數(shù)據(jù)庫更新操作提交的時候才對數(shù)據(jù)作沖突檢測。如果檢測的結果出現(xiàn)了與預期數(shù)據(jù)不一致的情況,則返回失敗信息。

樂觀鎖大多數(shù)是基于數(shù)據(jù)版本(version)的記錄機制實現(xiàn)的。何謂數(shù)據(jù)版本號?

即為數(shù)據(jù)增加一個版本標識,在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表添加一個 “version”字段來實現(xiàn)讀取出數(shù)據(jù)時,將此版本號一同讀出,之后更新時,對此版本號加 1。

在更新過程中,會對版本號進行比較,如果是一致的,沒有發(fā)生改變,則會成功執(zhí)行本次操作;如果版本號不一致,則會更新失敗。

為了更好的理解數(shù)據(jù)庫樂觀鎖在實際項目中的使用,這里也就舉了業(yè)界老生常談的庫存例子。

一個電商平臺都會存在商品的庫存,當用戶進行購買的時候就會對庫存進行操作(庫存減 1 代表已經(jīng)賣出了一件)。

如果只是一個用戶進行操作數(shù)據(jù)庫本身就能保證用戶操作的正確性,而在并發(fā)的情況下就會產(chǎn)生一些意想不到的問題。

比如兩個用戶同時購買一件商品,在數(shù)據(jù)庫層面實際操作應該是庫存進行減 2 操作。

但是由于高并發(fā)的情況,第一個用戶購買完成進行數(shù)據(jù)讀取當前庫存并進行減 1 操作,由于這個操作沒有完全執(zhí)行完成。

第二個用戶就進入購買相同商品,此時查詢出的庫存可能是未減 1 操作的庫存導致了臟數(shù)據(jù)的出現(xiàn)【線程不安全操作】。

數(shù)據(jù)庫樂觀鎖也能保證線程安全,通常代碼層面我們都會這樣做:

  1. select goods_num from goods where goods_name = "小本子"
  2. update goods set goods_num = goods_num -1 where goods_name = "小本子"

上面的 SQL 是一組的,通常先查詢出當前的 goods_num,然后再 goods_num 上進行減 1 的操作修改庫存。

當并發(fā)的情況下,這條語句可能導致原本庫存為 3 的一個商品經(jīng)過兩個人購買還剩下 2 庫存的情況就會導致商品的多賣。那么數(shù)據(jù)庫樂觀鎖是如何實現(xiàn)的呢?

首先定義一個 version 字段用來當作一個版本號,每次的操作就會變成這樣:

  1. select goods_num,version from goods where goods_name = "小本子"
  2. update goods set goods_num = goods_num -1,version =查詢的version值自增 where goods_name ="小本子" and version=查詢出來的version; 

其實,借助更新時間戳(updated_at)也可以實現(xiàn)樂觀鎖,和采用 version 字段的方式相似。

更新操作執(zhí)行前線獲取記錄當前的更新時間,在提交更新時,檢測當前更新時間是否與更新開始時獲取的更新時間戳相等。

悲觀鎖

除了可以通過增刪操作數(shù)據(jù)庫表中的記錄以外,我們還可以借助數(shù)據(jù)庫中自帶的鎖來實現(xiàn)分布式鎖。

在查詢語句后面增加 FOR UPDATE,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加悲觀鎖,也稱排他鎖。當某條記錄被加上悲觀鎖之后,其它線程也就無法再改行上增加悲觀鎖。

悲觀鎖,與樂觀鎖相反,總是假設最壞的情況,它認為數(shù)據(jù)的更新在大多數(shù)情況下是會產(chǎn)生沖突的。

在使用悲觀鎖的同時,我們需要注意一下鎖的級別。MySQL InnoDB 引起在加鎖的時候,只有明確地指定主鍵(或索引)的才會執(zhí)行行鎖 (只鎖住被選取的數(shù)據(jù)),否則 MySQL 將會執(zhí)行表鎖(將整個數(shù)據(jù)表單給鎖住)。

在使用悲觀鎖時,我們必須關閉 MySQL 數(shù)據(jù)庫的自動提交屬性(參考下面的示例),因為 MySQL 默認使用 autocommit 模式。

也就是說,當你執(zhí)行一個更新操作后,MySQL 會立刻將結果進行提交。

  1. mysql> SET AUTOCOMMIT = 0; 
  2. Query OK, 0 rows affected (0.00 sec) 

這樣在使用 FOR UPDATE 獲得鎖之后可以執(zhí)行相應的業(yè)務邏輯,執(zhí)行完之后再使用 COMMIT 來釋放鎖。

我們不妨沿用前面的 database_lock 表來具體表述一下用法。假設有一線程A需要獲得鎖并執(zhí)行相應的操作。

那么它的具體步驟如下:

  1. STEP1 - 獲取鎖:SELECT * FROM database_lock WHERE id = 1 FOR UPDATE;。 
  2. STEP2 - 執(zhí)行業(yè)務邏輯。 
  3. STEP3 - 釋放鎖:COMMIT。 

作者:凌晶

簡介:生活中的段子手,目前就職于一家地產(chǎn)公司做 DevOPS 相關工作, 曾在大型互聯(lián)網(wǎng)公司做高級運維工程師,熟悉 Linux 運維,Python 運維開發(fā),Java 開發(fā),DevOPS 常用開發(fā)組件等,個人公眾號:stromling,歡迎來撩我哦!

 

責任編輯:武曉燕 來源: 51CTO博客
相關推薦

2010-11-25 09:06:37

Web開發(fā)函數(shù)式編程

2017-10-24 11:28:23

Zookeeper分布式鎖架構

2021-02-28 07:49:28

Zookeeper分布式

2021-10-25 10:21:59

ZK分布式鎖ZooKeeper

2021-07-16 07:57:34

ZooKeeperCurator源碼

2022-10-27 10:44:14

分布式Zookeeper

2021-07-08 09:21:17

ZooKeeper分布式鎖 Curator

2020-11-16 12:55:41

Redis分布式鎖Zookeeper

2019-07-16 09:22:10

RedisZookeeper分布式鎖

2022-07-25 06:44:19

ZooKeeper分布式鎖

2019-06-19 15:40:06

分布式鎖RedisJava

2020-05-12 14:03:51

RedisZooKeeper分布式鎖

2019-10-10 09:16:34

Zookeeper架構分布式

2015-05-18 09:59:48

ZooKeeper分布式計算Hadoop

2024-01-09 09:27:08

RedLock分布式鎖Redis

2020-06-04 15:03:27

擺地攤互聯(lián)網(wǎng)風口

2021-07-10 10:02:30

ZooKeeperCurator并發(fā)

2018-01-25 19:01:47

Zookeeper分布式數(shù)據(jù)

2024-11-28 15:11:28

2020-12-15 06:57:24

java服務器
點贊
收藏

51CTO技術棧公眾號