<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
Java中的鎖主要包括synchronized鎖和JUC包中的鎖,這些鎖都是針對單個JVM範例上的鎖,對於分散式環境如果我們需要加鎖就顯得無能為力。
在單個JVM範例上,鎖的競爭者通常是一些不同的執行緒,而在分散式環境中,鎖的競爭者通常是一些不同的執行緒或者程序。如何實現在分散式環境中對一個物件進行加鎖呢?答案就是分散式鎖。
目前分散式鎖的實現方案主要包括三種:
基於資料庫實現分散式鎖:主要是利用資料庫的唯一索引來實現,唯一索引天然具有排他性,這剛好符合我們對鎖的要求:同一時刻只能允許一個競爭者獲取鎖。加鎖時我們在資料庫中插入一條鎖記錄,利用業務id進行防重。當第一個競爭者加鎖成功後,第二個競爭者再來加鎖就會丟擲唯一索引衝突,如果丟擲這個異常,我們就判定當前競爭者加鎖失敗。防重業務id需要我們自己來定義,例如我們的鎖物件是一個方法,則我們的業務防重id就是這個方法的名字,如果鎖定的物件是一個類,則業務防重id就是這個類名。
基於快取實現分散式鎖:理論上來說使用快取來實現分散式鎖的效率最高,加鎖速度最快,因為Redis幾乎都是純記憶體操作,而基於資料庫的方案和基於Zookeeper的方案都會涉及到磁碟檔案IO,效率相對低下。一般使用Redis來實現分散式鎖都是利用Redis的SETNX key value這個命令,只有當key不存在時才會執行成功,如果key已經存在則命令執行失敗。
基於Zookeeper:Zookeeper一般用作設定中心,其實現分散式鎖的原理和Redis類似,我們在Zookeeper中建立瞬時節點,利用節點不能重複建立的特性來保證排他性。
在實現分散式鎖的時候我們需要考慮一些問題,例如:分散式鎖是否可重入,分散式鎖的釋放時機,分散式鎖伺服器端是否有單點問題等。
上面已經分析了基於資料庫實現分散式鎖的基本原理:通過唯一索引保持排他性,加鎖時插入一條記錄,解鎖是刪除這條記錄。下面我們就簡要實現一下基於資料庫的分散式鎖。
表設計
CREATE TABLE `distributed_lock` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `unique_mutex` varchar(255) NOT NULL COMMENT '業務防重id', `holder_id` varchar(255) NOT NULL COMMENT '鎖持有者id', `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `mutex_index` (`unique_mutex`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
id欄位是資料庫的自增id,unique_mutex欄位就是我們的防重id,也就是加鎖的物件,此物件唯一。在這張表上我們加了一個唯一索引,保證unique_mutex唯一性。holder_id代表競爭到鎖的持有者id。
加鎖
insert into distributed_lock(unique_mutex, holder_id) values (‘unique_mutex', ‘holder_id');
如果當前sql執行成功代表加鎖成功,如果丟擲唯一索引異常(DuplicatedKeyException)則代表加鎖失敗,當前鎖已經被其他競爭者獲取。
解鎖
delete from methodLock where unique_mutex=‘unique_mutex' and holder_id=‘holder_id';
解鎖很簡單,直接刪除此條記錄即可。
分析
是否可重入:就以上的方案來說,我們實現的分散式鎖是不可重入的,即是是同一個競爭者,在獲取鎖後未釋放鎖之前再來加鎖,一樣會加鎖失敗,因此是不可重入的。解決不可重入問題也很簡單:加鎖時判斷記錄中是否存在unique_mutex的記錄,如果存在且holder_id和當前競爭者id相同,則加鎖成功。這樣就可以解決不可重入問題。
鎖釋放時機:設想如果一個競爭者獲取鎖時候,程序掛了,此時distributed_lock表中的這條記錄就會一直存在,其他競爭者無法加鎖。為了解決這個問題,每次加鎖之前我們先判斷已經存在的記錄的建立時間和當前系統時間之間的差是否已經超過超時時間,如果已經超過則先刪除這條記錄,再插入新的記錄。另外在解鎖時,必須是鎖的持有者來解鎖,其他競爭者無法解鎖。這點可以通過holder_id欄位來判定。
資料庫單點問題:單個資料庫容易產生單點問題:如果資料庫掛了,我們的鎖服務就掛了。對於這個問題,可以考慮實現資料庫的高可用方案,例如MySQL的MHA高可用解決方案。
前置知識:
Zookeeper的資料儲存結構就像一棵樹,這棵樹由節點組成,這種節點叫做Znode。
Znode分為四種型別:
Zookeeper分散式鎖恰恰應用了臨時順序節點。具體如何實現呢?讓我們來看一看詳細步驟:
獲取鎖
首先,在Zookeeper當中建立一個持久節點ParentLock。當第一個使用者端想要獲得鎖時,需要在ParentLock這個節點下面建立一個臨時順序節點 Lock1。
之後,Client1查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock1是不是順序最靠前的一個。如果是第一個節點,則成功獲得鎖。
這時候,如果再有一個使用者端 Client2 前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock2。
Client2查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock2是不是順序最靠前的一個,結果發現節點Lock2並不是最小的。
於是,Client2向排序僅比它靠前的節點Lock1註冊Watcher,用於監聽Lock1節點是否存在。這意味著Client2搶鎖失敗,進入了等待狀態。
這時候,如果又有一個使用者端Client3前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock3。
Client3查詢ParentLock下面所有的臨時順序節點並排序,判斷自己所建立的節點Lock3是不是順序最靠前的一個,結果同樣發現節點Lock3並不是最小的。
於是,Client3向排序僅比它靠前的節點Lock2註冊Watcher,用於監聽Lock2節點是否存在。這意味著Client3同樣搶鎖失敗,進入了等待狀態。
這樣一來,Client1得到了鎖,Client2監聽了Lock1,Client3監聽了Lock2。這恰恰形成了一個等待佇列,很像是Java當中ReentrantLock(可重入鎖)所依賴的AQS(AbstractQueuedSynchronizer)。
獲得鎖的過程大致就是這樣,那麼Zookeeper如何釋放鎖呢?
釋放鎖的過程很簡單,只需要釋放對應的子節點就好。
釋放鎖
釋放鎖分為兩種情況:
1.任務完成,使用者端顯示釋放
當任務完成時,Client1會顯示呼叫刪除節點Lock1的指令。
2.任務執行過程中,使用者端崩潰
獲得鎖的Client1在任務執行過程中,如果Duang的一聲崩潰,則會斷開與Zookeeper伺服器端的連結。根據臨時節點的特性,相關聯的節點Lock1會隨之自動刪除。
由於Client2一直監聽著Lock1的存在狀態,當Lock1節點被刪除,Client2會立刻收到通知。這時候Client2會再次查詢ParentLock下面的所有節點,確認自己建立的節點Lock2是不是目前最小的節點。如果是最小,則Client2順理成章獲得了鎖。
同理,如果Client2也因為任務完成或者節點崩潰而刪除了節點Lock2,那麼Client3就會接到通知。
最終,Client3成功得到了鎖。
使用Zookeeper實現分散式鎖的大致流程就是這樣。
分析
解決不可重入:使用者端加鎖時將主機和執行緒資訊寫入鎖中,下一次再來加鎖時直接和序列最小的節點對比,如果相同,則加鎖成功,鎖重入。
鎖釋放時機:由於我們建立的節點是順序臨時節點,當用戶端獲取鎖成功之後突然session對談斷開,ZK會自動刪除這個臨時節點。
單點問題:ZK是叢集部署的,主要一半以上的機器存活,就可以保證服務可用性。
Zookeeper第三方使用者端curator中已經實現了基於Zookeeper的分散式鎖。利用curator加鎖和解鎖的程式碼如下:
@Autowired private CuratorFramework curatorFramework; // 加鎖,支援超時,可重入 public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { // InterProcessMutex interProcessMutex= new InterProcessMutex(curatorFramework, "/ParenLock"); try { return interProcessMutex.acquire(timeout, unit); } catch (Exception e) { e.printStackTrace(); } return true; } // 解鎖 public boolean unlock() { InterProcessMutex interProcessMutex= new InterProcessMutex(curatorFramework, "/ParenLock"); try { interProcessMutex.release(); } catch (Throwable e) { log.error(e.getMessage(), e); } finally { executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS); } return true; }
最常用的鎖:
InterProcessMutex
:分散式可重入排它鎖InterProcessSemaphoreMutex
:分散式排它鎖InterProcessReadWriteLock
:分散式讀寫鎖加鎖
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 stringRedisTemplate Redis使用者端 * @param lockKey 鎖的key * @param requestId 競爭者id * @param expireTime 鎖超時時間,超時之後鎖自動釋放 * @return */ public static boolean getDistributedLock(StringRedisTemplate stringRedisTemplate, String lockKey, String requestId, int expireTime) { return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS); } }
可以看到,我們加鎖就一行程式碼:
stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
這個setIfAbsent()方法一共五個形參:
總的來說,執行上面的setIfAbsent()方法就只會導致兩種結果:
解鎖
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 釋放分散式鎖 * @param stringRedisTemplate Redis使用者端 * @param lockKey 鎖 * @param requestId 鎖持有者id * @return 是否釋放成功 */ public static boolean releaseDistributedLock(StringRedisTemplate stringRedisTemplate, 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"; Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(lockKey), requestId); return RELEASE_SUCCESS.equals(result); } }
解鎖的步驟:
注意到這裡解鎖其實是分為2個步驟,涉及到解鎖操作的一個原子性操作問題。這也是為什麼我們解鎖的時候用Lua指令碼來實現,因為Lua指令碼可以保證操作的原子性。那麼這裡為什麼需要保證這兩個步驟的操作是原子操作呢?
設想:假設當前鎖的持有者是競爭者1,競爭者1來解鎖,成功執行第1步,判斷自己就是鎖持有者,這是還未執行第2步。這是鎖過期了,然後競爭者2對這個key進行了加鎖。加鎖完成後,競爭者1又來執行第2步,此時錯誤產生了:競爭者1解鎖了不屬於自己持有的鎖。可能會有人問為什麼競爭者1執行完第1步之後突然停止了呢?這個問題其實很好回答,例如競爭者1所在的JVM發生了GC停頓,導致競爭者1的執行緒停頓。這樣的情況發生的概率很低,但是請記住即使只有萬分之一的概率,線上上環境中完全可能發生。因此必須保證這兩個步驟的操作是原子操作。
分析
redis分散式鎖,更詳細的可以參考:分散式鎖(Redisson)原理分析
方案 | 理解難易程度 | 實現的複雜度 | 效能 | 可靠性 | 優點 | 缺點 |
---|---|---|---|---|---|---|
基於資料庫 | 容易 | 複雜 | 差 | 不可靠 | ||
基於快取(Redis) | 一般 | 一般 | 高 | 可靠 | Set和Del指令效能較高 | 1.實現複雜,需要考慮超時,原子性,誤刪等情形。2.沒有等待鎖的佇列,只能在使用者端自旋來等待,效率低下。(但是現在有Redisson這兩缺點就相當於沒有了) |
基於Zookeeper | 難 | 簡單 | 一般 | 一般 | 1.有封裝好的框架,容易實現2.有等待鎖的佇列,大大提升搶鎖效率。 | 新增和刪除節點效能較低 |
以上為個人經驗,希望能給大家一個參考,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45