relation does not exist报错是什么意思_为什么Zookeeper天生就是一副分布式锁的胚子?...
什么是分布式鎖?分布式鎖是控制分布式系統之間同步訪問共享資源的一種方式。在分布式系統中,常常需要協調他們的動作。
圖片來自 Pexels
如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那么訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分布式鎖。
為什么要使用分布式鎖
為了保證一個方法或屬性在高并發情況下的同一時間只能被同一個線程執行。
在傳統單體應用單機部署的情況下,可以使用 Java 并發處理相關的 API(如 ReentrantLock 或 Synchronized)進行互斥控制;在單機環境中,Java 中提供了很多并發處理相關的 API。
但是,隨著業務發展的需要,原單體單機部署的系統被演化成分布式集群系統后,由于分布式系統多線程、多進程并且分布在不同機器上,這將使原單機部署情況下的并發控制鎖策略失效,單純的 Java API 并不能提供分布式鎖的能力。
為了解決這個問題就需要一種跨 JVM 的互斥機制來控制共享資源的訪問,這就是分布式鎖要解決的問題!
舉個例子:機器 A,機器 B 是一個集群。A,B 兩臺機器上的程序都是一樣的,具備高可用性能。
A,B 機器都有一個定時任務,每天晚上凌晨 2 點需要執行一個定時任務,但是這個定時任務只能執行一遍,否則的話就會報錯。
那 A,B 兩臺機器在執行的時候,就需要搶鎖,誰搶到鎖,誰執行,誰搶不到,就不用執行了!
鎖的處理
鎖的處理方式如下:單個應用中使用鎖:(單進程多線程)Synchronize。
分布式鎖控制分布式系統之間同步訪問資源的一種方式。
分布式鎖是控制分布式系統之間同步訪問共享資源的一種方式。
分布式鎖的實現
分布式鎖的實現方式如下:
基于數據的樂觀鎖實現分布式鎖
基于 Zookeeper 臨時節點的分布式鎖
基于 Redis 的分布式鎖
Redis 的分布式鎖
獲取鎖
在 set 命令中,有很多選項可以用來修改命令的行為,以下是 set 命令可用選項的基本語法:redis?127.0.0.1:6379>SET?KEY?VALUE?[EX?seconds]?[PX?milliseconds]?[NX|XX]-?EX?seconds?設置指定的到期時間(單位為秒)
-?PX?milliseconds?設置指定的到期時間(單位毫秒)
-?NX:?僅在鍵不存在時設置鍵
-?XX:?只有在鍵已存在時設置
方式 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 出現問題,就是出現死鎖的情況,所以推薦方式 1。
釋放鎖
方式 1:del 命令實現public?static?void?releaseLock(String?lockKey,String?requestId)?{???if?(requestId.equals(jedis.get(lockKey)))?{
???????jedis.del(lockKey);
??}
}
方式 2:Redis+Lua 腳本實現(推薦)
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;
??}
Zookeeper 的分布式鎖
Zookeeper?分布式鎖實現原理
理解了鎖的原理后,就會發現,Zookeeper 天生就是一副分布式鎖的胚子。首先,Zookeeper 的每一個節點,都是一個天然的順序發號器。在每一個節點下面創建子節點時,只要選擇的創建類型是有序(EPHEMERAL_SEQUENTIAL 臨時有序或者 PERSISTENT_SEQUENTIAL 永久有序)類型,那么,新的子節點后面,會加上一個次序編號。這個次序編號,是上一個生成的次序編號加 1,比如,創建一個用于發號的節點“/test/lock”,然后以他為父親節點,在這個父節點下面創建相同前綴的子節點。假定相同的前綴為“/test/lock/seq-”,在創建子節點時,同時指明是有序類型。如果是第一個創建的子節點,那么生成的子節點為 /test/lock/seq-0000000000,下一個節點則為 /test/lock/seq-0000000001,依次類推,等等。其次,Zookeeper 節點的遞增性,可以規定節點編號最小的那個獲得鎖。一個 Zookeeper 分布式鎖,首先需要創建一個父節點,盡量是持久節點(PERSISTENT 類型),然后每個要獲得鎖的線程都會在這個節點下創建個臨時順序節點,由于序號的遞增性,可以規定排號最小的那個獲得鎖。所以,每個線程在嘗試占用鎖之前,首先判斷自己是排號是不是當前最小,如果是,則獲取鎖。第三,Zookeeper 的節點監聽機制,可以保障占有鎖的方式有序而且高效。每個線程搶占鎖之前,先搶號創建自己的 ZNode。同樣,釋放鎖的時候,就需要刪除搶號的 Znode。搶號成功后,如果不是排號最小的節點,就處于等待通知的狀態。等誰的通知呢?不需要其他人,只需要等前一個 Znode 的通知就可以了。當前一個 Znode 刪除的時候,就是輪到了自己占有鎖的時候。第一個通知第二個、第二個通知第三個,擊鼓傳花似的依次向后。Zookeeper 的節點監聽機制,可以說能夠非常完美的,實現這種擊鼓傳花似的信息傳遞。具體的方法是,每一個等通知的 Znode 節點,只需要監聽 linsten 或者 watch 監視排號在自己前面那個,而且緊挨在自己前面的那個節點。只要上一個節點被刪除了,就進行再一次判斷,看看自己是不是序號最小的那個節點,如果是,則獲得鎖。為什么說 Zookeeper 的節點監聽機制,可以說是非常完美呢?一條龍式的首尾相接,后面監視前面,就不怕中間截斷嗎?比如,在分布式環境下,由于網絡的原因,或者服務器掛了或者其他的原因,如果前面的那個節點沒能被程序刪除成功,后面的節點不就永遠等待么?其實,Zookeeper 的內部機制,能保證后面的節點能夠正常的監聽到刪除和獲得鎖。在創建取號節點的時候,盡量創建臨時 Znode 節點而不是永久 Znode 節點。一旦這個 Znode 的客戶端與 Zookeeper 集群服務器失去聯系,這個臨時 Znode 也將自動刪除。排在它后面的那個節點,也能收到刪除事件,從而獲得鎖。說 Zookeeper 的節點監聽機制,是非常完美的。還有一個原因。Zookeeper 這種首尾相接,后面監聽前面的方式,可以避免羊群效應。所謂羊群效應就是每個節點掛掉,所有節點都去監聽,然后做出反映,這樣會給服務器帶來巨大壓力,所以有了臨時順序節點,當一個節點掛掉,只有它后面的那一個節點才做出反映。Zookeeper?分布式鎖實現示例
Zookeeper 是通過臨時節點來實現分布式鎖:
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()?{
???????//?業務邏輯:?秒殺
???????System.out.println("*********業務方法開始************\n");
???????System.out.println("當前的值:?"?+?NUMBER);
???????NUMBER--;
???????try?{
???????????Thread.sleep(2000);
??????}?catch?(InterruptedException?e)?{
???????????e.printStackTrace();
??????}
???????System.out.println("*********業務方法結束************\n");
??}
???//?這里使用@Test會報錯
???public?static?void?main(String[]?args)?{
???????//?定義重試的側策略?1000?等待的時間(毫秒)?10?重試的次數
???????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();
??????}
??}
}
基于數據的分布式鎖
我們在討論使用分布式鎖的時候往往首先排除掉基于數據庫的方案,本能的會覺得這個方案不夠“高級”。從性能的角度考慮,基于數據庫的方案性能確實不夠優異,整體性能對比:緩存>Zookeeper、etcd>數據庫。也有人提出基于數據庫的方案問題很多,不太可靠。數據庫的方案可能并不適合于頻繁寫入的操作。下面我們來了解一下基于數據庫(MySQL)的方案,一般分為三類:基于表記錄
樂觀鎖
悲觀鎖
基于表記錄
要實現分布式鎖,最簡單的方式可能就是直接創建一張鎖表,然后通過操作該表中的數據來實現了。當我們想要獲得鎖的時候,就可以在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。為了更好的演示,我們先創建一張數據庫表,參考如下:
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='數據庫分布式鎖表';
①獲得鎖
我們可以插入一條數據:
INSERT?INTO?database_lock(resource,?description)?VALUES?(1,?'lock');因為表 database_lock 中 resource 是唯一索引,所以其他請求提交到數據庫,就會報錯,并不會插入成功,只有一個可以插入。插入成功,我們就獲取到鎖。
②刪除鎖
INSERT?INTO?database_lock(resource,?description)?VALUES?(1,?'lock');這種實現方式非常的簡單,但是需要注意以下幾點:①這種鎖沒有失效時間,一旦釋放鎖的操作失敗就會導致鎖記錄一直在數據庫中,其他線程無法獲得鎖。這個缺陷也很好解決,比如可以做一個定時任務去定時清理。②這種鎖的可靠性依賴于數據庫。建議設置備庫,避免單點,進一步提高可靠性。③這種鎖是非阻塞的,因為插入數據失敗之后會直接報錯,想要獲得鎖就需要再次操作。如果需要阻塞式的,可以弄個 for 循環、while 循環之類的,直至 INSERT 成功再返回。④這種鎖也是非可重入的,因為同一個線程在沒有釋放鎖之前無法再次獲得鎖,因為數據庫中已經存在同一份記錄了。想要實現可重入鎖,可以在數據庫中添加一些字段,比如獲得鎖的主機信息、線程信息等。那么在再次獲得鎖的時候可以先查詢數據,如果當前的主機信息和線程信息等能被查到的話,可以直接把鎖分配給它。
樂觀鎖
顧名思義,系統認為數據的更新在大多數情況下是不會產生沖突的,只在數據庫更新操作提交的時候才對數據作沖突檢測。如果檢測的結果出現了與預期數據不一致的情況,則返回失敗信息。樂觀鎖大多數是基于數據版本(version)的記錄機制實現的。何謂數據版本號?即為數據增加一個版本標識,在基于數據庫表的版本解決方案中,一般是通過為數據庫表添加一個 “version”字段來實現讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加 1。在更新過程中,會對版本號進行比較,如果是一致的,沒有發生改變,則會成功執行本次操作;如果版本號不一致,則會更新失敗。為了更好的理解數據庫樂觀鎖在實際項目中的使用,這里也就舉了業界老生常談的庫存例子。一個電商平臺都會存在商品的庫存,當用戶進行購買的時候就會對庫存進行操作(庫存減 1 代表已經賣出了一件)。如果只是一個用戶進行操作數據庫本身就能保證用戶操作的正確性,而在并發的情況下就會產生一些意想不到的問題。比如兩個用戶同時購買一件商品,在數據庫層面實際操作應該是庫存進行減 2 操作。但是由于高并發的情況,第一個用戶購買完成進行數據讀取當前庫存并進行減 1 操作,由于這個操作沒有完全執行完成。第二個用戶就進入購買相同商品,此時查詢出的庫存可能是未減 1 操作的庫存導致了臟數據的出現【線程不安全操作】。數據庫樂觀鎖也能保證線程安全,通常代碼層面我們都會這樣做:
select?goods_num?from?goods?where?goods_name?=?"小本子";update?goods?set?goods_num?=?goods_num?-1?where?goods_name?=?"小本子";
上面的 SQL 是一組的,通常先查詢出當前的 goods_num,然后再 goods_num 上進行減 1 的操作修改庫存。當并發的情況下,這條語句可能導致原本庫存為 3 的一個商品經過兩個人購買還剩下 2 庫存的情況就會導致商品的多賣。那么數據庫樂觀鎖是如何實現的呢?
首先定義一個 version 字段用來當作一個版本號,每次的操作就會變成這樣:
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)也可以實現樂觀鎖,和采用 version 字段的方式相似。更新操作執行前線獲取記錄當前的更新時間,在提交更新時,檢測當前更新時間是否與更新開始時獲取的更新時間戳相等。
悲觀鎖
除了可以通過增刪操作數據庫表中的記錄以外,我們還可以借助數據庫中自帶的鎖來實現分布式鎖。在查詢語句后面增加 FOR UPDATE,數據庫會在查詢過程中給數據庫表增加悲觀鎖,也稱排他鎖。當某條記錄被加上悲觀鎖之后,其它線程也就無法再改行上增加悲觀鎖。悲觀鎖,與樂觀鎖相反,總是假設最壞的情況,它認為數據的更新在大多數情況下是會產生沖突的。在使用悲觀鎖的同時,我們需要注意一下鎖的級別。MySQL InnoDB 引起在加鎖的時候,只有明確地指定主鍵(或索引)的才會執行行鎖 (只鎖住被選取的數據),否則 MySQL 將會執行表鎖(將整個數據表單給鎖住)。在使用悲觀鎖時,我們必須關閉 MySQL 數據庫的自動提交屬性(參考下面的示例),因為 MySQL 默認使用 autocommit 模式。也就是說,當你執行一個更新操作后,MySQL 會立刻將結果進行提交。mysql>?SET?AUTOCOMMIT?=?0;Query?OK,?0?rows?affected?(0.00?sec)
這樣在使用 FOR UPDATE 獲得鎖之后可以執行相應的業務邏輯,執行完之后再使用 COMMIT 來釋放鎖。我們不妨沿用前面的 database_lock 表來具體表述一下用法。假設有一線程A需要獲得鎖并執行相應的操作。
那么它的具體步驟如下:
STEP1?-?獲取鎖:SELECT?*?FROM?database_lock?WHERE?id?=?1?FOR?UPDATE;。STEP2?-?執行業務邏輯。
STEP3?-?釋放鎖:COMMIT。
作者:凌晶
簡介:生活中的段子手,目前就職于一家地產公司做 DevOPS 相關工作, 曾在大型互聯網公司做高級運維工程師,熟悉 Linux 運維,Python 運維開發,Java 開發,DevOPS 常用開發組件等,個人公眾號:stromling,歡迎來撩我哦!
編輯:陶家龍
出處:https://blog.51cto.com/lingjing/2474793
精彩文章推薦:
你再不知道分布式事務,我就真的生氣了!Kafka架構原理,也就這么回事!精通那么多技術,為何還是寫出一堆“屎山”? 創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的relation does not exist报错是什么意思_为什么Zookeeper天生就是一副分布式锁的胚子?...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 教你使用手机控制电脑手机 如何 控制电脑
- 下一篇: loadrunner录制事件为0_测试工