分布式鎖的三種實(shí)現(xiàn)方式
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
? 作者?|? BarryW
來(lái)源 |? urlify.cn/JrAZBb
76套java從入門到精通實(shí)戰(zhàn)課程分享
分布式鎖三種實(shí)現(xiàn)方式:
1 2 3 |
|
?
一, 基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖
1. 悲觀鎖
利用select … where … for update 排他鎖
注意: 其他附加功能與實(shí)現(xiàn)一基本一致,這里需要注意的是“where name=lock ”,name字段必須要走索引,否則會(huì)鎖表。有些情況下,比如表不大,mysql優(yōu)化器會(huì)不走這個(gè)索引,導(dǎo)致鎖表問(wèn)題。
?
2. 樂(lè)觀鎖
所謂樂(lè)觀鎖與前邊最大區(qū)別在于基于CAS思想,是不具有互斥性,不會(huì)產(chǎn)生鎖等待而消耗資源,操作過(guò)程中認(rèn)為不存在并發(fā)沖突,只有update version失敗后才能覺(jué)察到。我們的搶購(gòu)、秒殺就是用了這種實(shí)現(xiàn)以防止超賣。
通過(guò)增加遞增的版本號(hào)字段實(shí)現(xiàn)樂(lè)觀鎖
?
二, 基于緩存(Redis等)實(shí)現(xiàn)分布式鎖
1. 使用命令介紹:
(1)SETNX
SETNX key val:當(dāng)且僅當(dāng)key不存在時(shí),set一個(gè)key為val的字符串,返回1;若key存在,則什么都不做,返回0。
(2)expire
expire key timeout:為key設(shè)置一個(gè)超時(shí)時(shí)間,單位為second,超過(guò)這個(gè)時(shí)間鎖會(huì)自動(dòng)釋放,避免死鎖。
(3)delete
delete key:刪除key
在使用Redis實(shí)現(xiàn)分布式鎖的時(shí)候,主要就會(huì)使用到這三個(gè)命令。
?
2. 實(shí)現(xiàn)思想:
(1)獲取鎖的時(shí)候,使用setnx加鎖,并使用expire命令為鎖添加一個(gè)超時(shí)時(shí)間,超過(guò)該時(shí)間則自動(dòng)釋放鎖,鎖的value值為一個(gè)隨機(jī)生成的UUID,通過(guò)此在釋放鎖的時(shí)候進(jìn)行判斷。
(2)獲取鎖的時(shí)候還設(shè)置一個(gè)獲取的超時(shí)時(shí)間,若超過(guò)這個(gè)時(shí)間則放棄獲取鎖。
(3)釋放鎖的時(shí)候,通過(guò)UUID判斷是不是該鎖,若是該鎖,則執(zhí)行delete進(jìn)行鎖釋放。
?
3. 分布式鎖的簡(jiǎn)單實(shí)現(xiàn)代碼:
1?/**
??2??*?分布式鎖的簡(jiǎn)單實(shí)現(xiàn)代碼??4??*/
??5?public?class?DistributedLock?{
??6?
??7?????private?final?JedisPool?jedisPool;
??8?
??9?????public?DistributedLock(JedisPool?jedisPool)?{
?10?????????this.jedisPool?=?jedisPool;
?11?????}
?12?
?13?????/**
?14??????*?加鎖
?15??????*?@param?lockName???????鎖的key
?16??????*?@param?acquireTimeout?獲取超時(shí)時(shí)間
?17??????*?@param?timeout????????鎖的超時(shí)時(shí)間
?18??????*?@return?鎖標(biāo)識(shí)
?19??????*/
?20?????public?String?lockWithTimeout(String?lockName,?long?acquireTimeout,?long?timeout)?{
?21?????????Jedis?conn?=?null;
?22?????????String?retIdentifier?=?null;
?23?????????try?{
?24?????????????//?獲取連接
?25?????????????conn?=?jedisPool.getResource();
?26?????????????//?隨機(jī)生成一個(gè)value
?27?????????????String?identifier?=?UUID.randomUUID().toString();
?28?????????????//?鎖名,即key值
?29?????????????String?lockKey?=?"lock:"?+?lockName;
?30?????????????//?超時(shí)時(shí)間,上鎖后超過(guò)此時(shí)間則自動(dòng)釋放鎖
?31?????????????int?lockExpire?=?(int)?(timeout?/?1000);
?32?
?33?????????????//?獲取鎖的超時(shí)時(shí)間,超過(guò)這個(gè)時(shí)間則放棄獲取鎖
?34?????????????long?end?=?System.currentTimeMillis()?+?acquireTimeout;
?35?????????????while?(System.currentTimeMillis()??36?????????????????if?(conn.setnx(lockKey,?identifier)?==?1)?{
?37?????????????????????conn.expire(lockKey,?lockExpire);
?38?????????????????????//?返回value值,用于釋放鎖時(shí)間確認(rèn)
?39?????????????????????retIdentifier?=?identifier;
?40?????????????????????return?retIdentifier;
?41?????????????????}
?42?????????????????//?返回-1代表key沒(méi)有設(shè)置超時(shí)時(shí)間,為key設(shè)置一個(gè)超時(shí)時(shí)間
?43?????????????????if?(conn.ttl(lockKey)?==?-1)?{
?44?????????????????????conn.expire(lockKey,?lockExpire);
?45?????????????????}
?46?
?47?????????????????try?{
?48?????????????????????Thread.sleep(10);
?49?????????????????}?catch?(InterruptedException?e)?{
?50?????????????????????Thread.currentThread().interrupt();
?51?????????????????}
?52?????????????}
?53?????????}?catch?(JedisException?e)?{
?54?????????????e.printStackTrace();
?55?????????}?finally?{
?56?????????????if?(conn?!=?null)?{
?57?????????????????conn.close();
?58?????????????}
?59?????????}
?60?????????return?retIdentifier;
?61?????}
?62?
?63?????/**
?64??????*?釋放鎖
?65??????*?@param?lockName???鎖的key
?66??????*?@param?identifier?釋放鎖的標(biāo)識(shí)
?67??????*?@return
?68??????*/
?69?????public?boolean?releaseLock(String?lockName,?String?identifier)?{
?70?????????Jedis?conn?=?null;
?71?????????String?lockKey?=?"lock:"?+?lockName;
?72?????????boolean?retFlag?=?false;
?73?????????try?{
?74?????????????conn?=?jedisPool.getResource();
?75?????????????while?(true)?{
?76?????????????????//?監(jiān)視lock,準(zhǔn)備開(kāi)始事務(wù)
?77?????????????????conn.watch(lockKey);
?78?????????????????//?通過(guò)前面返回的value值判斷是不是該鎖,若是該鎖,則刪除,釋放鎖
?79?????????????????if?(identifier.equals(conn.get(lockKey)))?{
?80?????????????????????Transaction?transaction?=?conn.multi();
?81?????????????????????transaction.del(lockKey);
?82?????????????????????List?
4.?測(cè)試剛才實(shí)現(xiàn)的分布式鎖
例子中使用50個(gè)線程模擬秒殺一個(gè)商品,使用–運(yùn)算符來(lái)實(shí)現(xiàn)商品減少,從結(jié)果有序性就可以看出是否為加鎖狀態(tài)。
模擬秒殺服務(wù),在其中配置了jedis線程池,在初始化的時(shí)候傳給分布式鎖,供其使用。
public?class?Service?{
????private?static?JedisPool?pool?=?null;
????private?DistributedLock?lock?=?new?DistributedLock(pool);
????int?n?=?500;
????static?{
????????JedisPoolConfig?config?=?new?JedisPoolConfig();
????????//?設(shè)置最大連接數(shù)
????????config.setMaxTotal(200);
????????//?設(shè)置最大空閑數(shù)
????????config.setMaxIdle(8);
????????//?設(shè)置最大等待時(shí)間
????????config.setMaxWaitMillis(1000?*?100);
????????//?在borrow一個(gè)jedis實(shí)例時(shí),是否需要驗(yàn)證,若為true,則所有jedis實(shí)例均是可用的
????????config.setTestOnBorrow(true);
????????pool?=?new?JedisPool(config,?"127.0.0.1",?6379,?3000);
????}
????public?void?seckill()?{
????????//?返回鎖的value值,供釋放鎖時(shí)候進(jìn)行判斷
????????String?identifier?=?lock.lockWithTimeout("resource",?5000,?1000);
????????System.out.println(Thread.currentThread().getName()?+?"獲得了鎖");
????????System.out.println(--n);
????????lock.releaseLock("resource",?identifier);
????}
}
?
模擬線程進(jìn)行秒殺服務(wù);
public?class?ThreadA?extends?Thread?{
????private?Service?service;
????public?ThreadA(Service?service)?{
????????this.service?=?service;
????}
????@Override
????public?void?run()?{
????????service.seckill();
????}
}
public?class?Test?{
????public?static?void?main(String[]?args)?{
????????Service?service?=?new?Service();
????????for?(int?i?=?0;?i?50;?i++)?{
????????????ThreadA?threadA?=?new?ThreadA(service);
????????????threadA.start();
????????}
????}
}
結(jié)果如下,結(jié)果為有序的:

若注釋掉使用鎖的部分:
public?void?seckill()?{
????//?返回鎖的value值,供釋放鎖時(shí)候進(jìn)行判斷
????//String?indentifier?=?lock.lockWithTimeout("resource",?5000,?1000);
????System.out.println(Thread.currentThread().getName()?+?"獲得了鎖");
????System.out.println(--n);
????//lock.releaseLock("resource",?indentifier);
}
從結(jié)果可以看出,有一些是異步進(jìn)行的:

?
三, 基于Zookeeper實(shí)現(xiàn)分布式鎖?
ZooKeeper是一個(gè)為分布式應(yīng)用提供一致性服務(wù)的開(kāi)源組件,它內(nèi)部是一個(gè)分層的文件系統(tǒng)目錄樹(shù)結(jié)構(gòu),規(guī)定同一個(gè)目錄下只能有一個(gè)唯一文件名。基于ZooKeeper實(shí)現(xiàn)分布式鎖的步驟如下:
(1)創(chuàng)建一個(gè)目錄mylock;
(2)線程A想獲取鎖就在mylock目錄下創(chuàng)建臨時(shí)順序節(jié)點(diǎn);
(3)獲取mylock目錄下所有的子節(jié)點(diǎn),然后獲取比自己小的兄弟節(jié)點(diǎn),如果不存在,則說(shuō)明當(dāng)前線程順序號(hào)最小,獲得鎖;
(4)線程B獲取所有節(jié)點(diǎn),判斷自己不是最小節(jié)點(diǎn),設(shè)置監(jiān)聽(tīng)比自己次小的節(jié)點(diǎn);
(5)線程A處理完,刪除自己的節(jié)點(diǎn),線程B監(jiān)聽(tīng)到變更事件,判斷自己是不是最小的節(jié)點(diǎn),如果是則獲得鎖。
這里推薦一個(gè)Apache的開(kāi)源庫(kù)Curator,它是一個(gè)ZooKeeper客戶端,Curator提供的InterProcessMutex是分布式鎖的實(shí)現(xiàn),acquire方法用于獲取鎖,release方法用于釋放鎖。
實(shí)現(xiàn)源碼如下:
import?lombok.extern.slf4j.Slf4j;
import?org.apache.commons.lang.StringUtils;
import?org.apache.curator.framework.CuratorFramework;
import?org.apache.curator.framework.CuratorFrameworkFactory;
import?org.apache.curator.retry.RetryNTimes;
import?org.apache.zookeeper.CreateMode;
import?org.apache.zookeeper.data.Stat;
import?org.springframework.beans.factory.annotation.Value;
import?org.springframework.context.annotation.Bean;
import?org.springframework.stereotype.Component;
/**
?*?分布式鎖Zookeeper實(shí)現(xiàn)
?*
?*/
@Slf4j
@Component
public?class?ZkLock?implements?DistributionLock?{
private?String?zkAddress?=?"zk_adress";
????private?static?final?String?root?=?"package?root";
????private?CuratorFramework?zkClient;
????private?final?String?LOCK_PREFIX?=?"/lock_";
????@Bean
????public?DistributionLock?initZkLock()?{
????????if?(StringUtils.isBlank(root))?{
????????????throw?new?RuntimeException("zookeeper?'root'?can't?be?null");
????????}
????????zkClient?=?CuratorFrameworkFactory
????????????????.builder()
????????????????.connectString(zkAddress)
????????????????.retryPolicy(new?RetryNTimes(2000,?20000))
????????????????.namespace(root)
????????????????.build();
????????zkClient.start();
????????return?this;
????}
????public?boolean?tryLock(String?lockName)?{
????????lockName?=?LOCK_PREFIX+lockName;
????????boolean?locked?=?true;
????????try?{
????????????Stat?stat?=?zkClient.checkExists().forPath(lockName);
????????????if?(stat?==?null)?{
????????????????log.info("tryLock:{}",?lockName);
????????????????stat?=?zkClient.checkExists().forPath(lockName);
????????????????if?(stat?==?null)?{
????????????????????zkClient
????????????????????????????.create()
????????????????????????????.creatingParentsIfNeeded()
????????????????????????????.withMode(CreateMode.EPHEMERAL)
????????????????????????????.forPath(lockName,?"1".getBytes());
????????????????}?else?{
????????????????????log.warn("double-check?stat.version:{}",?stat.getAversion());
????????????????????locked?=?false;
????????????????}
????????????}?else?{
????????????????log.warn("check?stat.version:{}",?stat.getAversion());
????????????????locked?=?false;
????????????}
????????}?catch?(Exception?e)?{
????????????locked?=?false;
????????}
????????return?locked;
????}
????public?boolean?tryLock(String?key,?long?timeout)?{
????????return?false;
????}
????public?void?release(String?lockName)?{
????????lockName?=?LOCK_PREFIX+lockName;
????????try?{
????????????zkClient
????????????????????.delete()
????????????????????.guaranteed()
????????????????????.deletingChildrenIfNeeded()
????????????????????.forPath(lockName);
????????????log.info("release:{}",?lockName);
????????}?catch?(Exception?e)?{
????????????log.error("刪除",?e);
????????}
????}
????public?void?setZkAddress(String?zkAddress)?{
????????this.zkAddress?=?zkAddress;
????}
}
優(yōu)點(diǎn):具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問(wèn)題。
缺點(diǎn):因?yàn)樾枰l繁的創(chuàng)建和刪除節(jié)點(diǎn),性能上不如Redis方式。
?
四,對(duì)比
數(shù)據(jù)庫(kù)分布式鎖實(shí)現(xiàn)
缺點(diǎn):
1.db操作性能較差,并且有鎖表的風(fēng)險(xiǎn)
2.非阻塞操作失敗后,需要輪詢,占用cpu資源;
3.長(zhǎng)時(shí)間不commit或者長(zhǎng)時(shí)間輪詢,可能會(huì)占用較多連接資源
Redis(緩存)分布式鎖實(shí)現(xiàn)
缺點(diǎn):
1.鎖刪除失敗 過(guò)期時(shí)間不好控制
2.非阻塞,操作失敗后,需要輪詢,占用cpu資源;
ZK分布式鎖實(shí)現(xiàn)
缺點(diǎn):性能不如redis實(shí)現(xiàn),主要原因是寫操作(獲取鎖釋放鎖)都需要在Leader上執(zhí)行,然后同步到follower。
總之:ZooKeeper有較好的性能和可靠性。
?
從理解的難易程度角度(從低到高)數(shù)據(jù)庫(kù) > 緩存 > Zookeeper
從實(shí)現(xiàn)的復(fù)雜性角度(從低到高)Zookeeper >= 緩存 > 數(shù)據(jù)庫(kù)
從性能角度(從高到低)緩存 > Zookeeper >= 數(shù)據(jù)庫(kù)
從可靠性角度(從高到低)Zookeeper > 緩存 > 數(shù)據(jù)庫(kù)
粉絲福利:Java從入門到入土學(xué)習(xí)路線圖
???

?長(zhǎng)按上方微信二維碼?2 秒
感謝點(diǎn)贊支持下哈?
