源:Java愛(ài)好者社區(qū):https://www.cnblogs.com/ldws/p/12155003.html
首先來(lái)由一個(gè)場(chǎng)景引入:
最近老板接了一個(gè)大單子,允許在某終端設(shè)備安裝我們的APP,終端設(shè)備廠商日活起碼得幾十萬(wàn)到百萬(wàn)級(jí)別,這個(gè)APP也是近期產(chǎn)品根據(jù)市場(chǎng)競(jìng)品分析設(shè)計(jì)出來(lái)的,幾個(gè)小碼農(nóng)通宵達(dá)旦開(kāi)發(fā)出來(lái)的,主要功能是在線購(gòu)物一站式服務(wù),后臺(tái)可以給各個(gè)商家分配權(quán)限,來(lái)維護(hù)需要售賣(mài)的商品信息。
老板大O:談下來(lái)不容易,接下來(lái)就是考慮如何吸引終端設(shè)備上更多的用戶(hù)注冊(cè)上來(lái),如何引導(dǎo)用戶(hù)購(gòu)買(mǎi),這塊就交給小P去負(fù)責(zé)了,需求盡快做,我明天出差!
產(chǎn)品小P:嘿嘿~,眼珠一轉(zhuǎn)兒,很容易就想到了,心里想:“這還不簡(jiǎn)單,起碼在首頁(yè)搞個(gè)活動(dòng)頁(yè)... ”。
技術(shù)小T:很快了解了產(chǎn)品的需求,目前小J主要負(fù)責(zé)這塊,找了前端和后端同學(xué)一起將活動(dòng)頁(yè)搞的快差不多了。
業(yè)務(wù)場(chǎng)景一出現(xiàn):
因?yàn)樾剛接手項(xiàng)目,正在吭哧吭哧對(duì)熟悉著代碼、部署架構(gòu)。在看代碼過(guò)程中發(fā)現(xiàn),下單這塊代碼可能會(huì)出現(xiàn)問(wèn)題,這可是分布式部署的,如果多個(gè)用戶(hù)同時(shí)購(gòu)買(mǎi)同一個(gè)商品,就可能導(dǎo)致商品出現(xiàn) 庫(kù)存超賣(mài) (數(shù)據(jù)不一致) 現(xiàn)象,對(duì)于這種情況代碼中并沒(méi)有做任何控制。
原來(lái)一問(wèn)才知道,以前他們都是售賣(mài)的虛擬商品,沒(méi)啥庫(kù)存一說(shuō),所以當(dāng)時(shí)沒(méi)有考慮那么多...
這次不一樣啊,這次是售賣(mài)的實(shí)體商品,那就有庫(kù)存這么一說(shuō)了,起碼要保證不能超過(guò)庫(kù)存設(shè)定的數(shù)量吧。
小T大眼對(duì)著屏幕,屏住呼吸,還好提前發(fā)現(xiàn)了這個(gè)問(wèn)題,趕緊想辦法修復(fù),不賺錢(qián)還賠錢(qián),老板不得瘋了,還想不想干了~
業(yè)務(wù)場(chǎng)景二出現(xiàn):
小T下面的一位兄弟正在壓測(cè),發(fā)現(xiàn)個(gè)小問(wèn)題,因?yàn)樵诮K端設(shè)備上跟鵝廠有緊密合作,調(diào)用他們的接口時(shí)需要獲取到access_token,但是這個(gè)access_token過(guò)期時(shí)間是2小時(shí),過(guò)期后需要重新獲取。
壓測(cè)時(shí)發(fā)現(xiàn)當(dāng)?shù)竭_(dá)過(guò)期時(shí)間時(shí),日志看刷出來(lái)好幾個(gè)不一樣的access_token,因?yàn)檫@個(gè)服務(wù)也是分布式部署的,多個(gè)節(jié)點(diǎn)同時(shí)發(fā)起了第三方接口請(qǐng)求導(dǎo)致。
雖然以最后一次獲取的access_token為準(zhǔn),也沒(méi)什么不良副作用,但是會(huì)導(dǎo)致多次不必要的對(duì)第三方接口的調(diào)用,也會(huì)短時(shí)間內(nèi)造成access_token的 重復(fù)無(wú)效獲取(重復(fù)工作)。
業(yè)務(wù)場(chǎng)景三出現(xiàn):
下單完成后,還要通知倉(cāng)儲(chǔ)物流,待用戶(hù)支付完成,支付回調(diào)有可能會(huì)將多條訂單消息發(fā)送到MQ,倉(cāng)儲(chǔ)服務(wù)會(huì)從MQ消費(fèi)訂單消息,此時(shí)就要 保證冪等性,對(duì)訂單消息做 去重 處理。
以上便于大家理解為什么要用分布式鎖才能解決,勾勒出的幾個(gè)業(yè)務(wù)場(chǎng)景。
上面的問(wèn)題無(wú)一例外,都是針對(duì)共享資源要求串行化處理,才能保證安全且合理的操作。
用一張圖來(lái)體驗(yàn)一下:
此時(shí),使用Java提供的Synchronized、ReentrantLock、ReentrantReadWriteLock...,僅能在單個(gè)JVM進(jìn)程內(nèi)對(duì)多線程對(duì)共享資源保證線程安全,在分布式系統(tǒng)環(huán)境下統(tǒng)統(tǒng)都不好使,心情是不是拔涼呀。
這個(gè)問(wèn)題得請(qǐng)教 分布式鎖 家族來(lái)支持一下,聽(tīng)說(shuō)他們家族內(nèi)有很多成員,每個(gè)成員都有這個(gè)分布式鎖功能,接下來(lái)就開(kāi)始探索一下。
聽(tīng)聽(tīng) Martin 大佬們給出的說(shuō)法:
Martin kleppmann 是英國(guó)劍橋大學(xué)的分布式系統(tǒng)的研究員,曾經(jīng)跟 Redis 之父 Antirez 進(jìn)行過(guò)關(guān)于 RedLock (Redis里分布式鎖的實(shí)現(xiàn)算法)是否安全的激烈討論。
他們討論了啥,整急眼了?
都能單獨(dú)寫(xiě)篇文章了
效率:
使用分布式鎖可以避免多個(gè)客戶(hù)端重復(fù)相同的工作,這些工作會(huì)浪費(fèi)資源。比如用戶(hù)支付完成后,可能會(huì)收到多次短信或郵件提醒。
比如業(yè)務(wù)場(chǎng)景二,重復(fù)獲取access_token。
對(duì)共享資源的操作是冪等性操作,無(wú)論你操作多少次都不會(huì)出現(xiàn)不同結(jié)果。
本質(zhì)上就是為了避免對(duì)共享資源重復(fù)操作,從而提高效率。
正確性:
使用分布式鎖同樣可以避免鎖失效的發(fā)生,一旦發(fā)生會(huì)引起正確性的破壞,可能會(huì)導(dǎo)致數(shù)據(jù)不一致,數(shù)據(jù)缺失或者其他嚴(yán)重的問(wèn)題。
比如業(yè)務(wù)場(chǎng)景一,商品庫(kù)存超賣(mài)問(wèn)題。
對(duì)共享資源的操作是非冪等性操作,多個(gè)客戶(hù)端操作共享資源會(huì)導(dǎo)致數(shù)據(jù)不一致。
以下是分布式鎖的一些特點(diǎn),分布式鎖家族成員并不一定都滿(mǎn)足這個(gè)要求,實(shí)現(xiàn)機(jī)制不大一樣。
互斥性: 分布式鎖要保證在多個(gè)客戶(hù)端之間的互斥。
可重入性:同一客戶(hù)端的相同線程,允許重復(fù)多次加鎖。
鎖超時(shí):和本地鎖一樣支持鎖超時(shí),防止死鎖。
非阻塞: 能與 ReentrantLock 一樣支持 trylock() 非阻塞方式獲得鎖。
支持公平鎖和非公平鎖:公平鎖是指按照請(qǐng)求加鎖的順序獲得鎖,非公平鎖真好相反請(qǐng)求加鎖是無(wú)序的。
分布式鎖家族實(shí)現(xiàn)者一覽:
思維導(dǎo)圖做了一個(gè)簡(jiǎn)單分類(lèi),不一定特別準(zhǔn)確,幾乎包含了分布式鎖各個(gè)組件實(shí)現(xiàn)者。
下面讓他們分別來(lái)做下自我介紹:
1、數(shù)據(jù)庫(kù)
排它鎖(悲觀鎖):基于 select * from table where xx=yy for update SQL語(yǔ)句來(lái)實(shí)現(xiàn),有很多缺陷,一般不推薦使用,后文介紹。
樂(lè)觀鎖:表中添加一個(gè)時(shí)間戳或者版本號(hào)的字段來(lái)實(shí)現(xiàn),update xx set version=new... where id=y and version=old 當(dāng)更新不成功,客戶(hù)端重試,重新讀取最新的版本號(hào)或時(shí)間戳,再次嘗試更新,類(lèi)似 CAS 機(jī)制,推薦使用。
2、Redis
特點(diǎn):CAP模型屬于AP | 無(wú)一致性算法 | 性能好
開(kāi)發(fā)常用,如果你的項(xiàng)目中正好使用了redis,不想引入額外的分布式鎖組件,推薦使用。
業(yè)界也提供了多個(gè)現(xiàn)成好用的框架予以支持分布式鎖,比如Redisson、spring-integration-redis、redis自帶的setnx命令,推薦直接使用。
另外,可基于redis命令和redis lua支持的原子特性,自行實(shí)現(xiàn)分布式鎖。
3、Zookeeper
特點(diǎn):CAP模型屬于CP | ZAB一致性算法實(shí)現(xiàn) | 穩(wěn)定性好
開(kāi)發(fā)常用,如果你的項(xiàng)目中正好使用了zk集群,推薦使用。
業(yè)界有Apache Curator框架提供了現(xiàn)成的分布式鎖功能,現(xiàn)成的,推薦直接使用。
另外,可基于Zookeeper自身的特性和原生Zookeeper API自行實(shí)現(xiàn)分布式鎖。
4、其他
Chubby,Google開(kāi)發(fā)的粗粒度分布鎖的服務(wù),但是并沒(méi)有開(kāi)源,開(kāi)放出了論文和一些相關(guān)文檔可以進(jìn)一步了解,出門(mén)百度一下獲取文檔,不做過(guò)多討論。
Tair,是阿里開(kāi)源的一個(gè)分布式KV存儲(chǔ)方案,沒(méi)有用過(guò),不做過(guò)多討論。
Etcd,CAP模型中屬于CP,Raft一致性算法實(shí)現(xiàn),沒(méi)有用過(guò),不做過(guò)多討論。
Hazelcast,是基于內(nèi)存的數(shù)據(jù)網(wǎng)格開(kāi)源項(xiàng)目,提供彈性可擴(kuò)展的分布式內(nèi)存計(jì)算,并且被公認(rèn)是提高應(yīng)用程序性能和擴(kuò)展性最好的方案,聽(tīng)上去很牛逼,但是沒(méi)用過(guò),不做過(guò)多討論。
當(dāng)然了,上面推薦的常用分布式鎖Zookeeper和Redis,使用時(shí)還需要根據(jù)具體的業(yè)務(wù)場(chǎng)景,做下權(quán)衡,實(shí)現(xiàn)功能上都能達(dá)到你要的效果,原理上有很大的不同。
畫(huà)外音: 你對(duì)哪個(gè)熟悉,原理也都了解,hold住,你就用哪個(gè)。
以「悲觀的心態(tài)」操作資源,無(wú)法獲得鎖成功,就一直阻塞著等待。
1、有一張資源鎖表
CREATE TABLE `resource_lock` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的資源名',
`owner` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖擁有者',
`desc` varchar(1024) NOT NULL DEFAULT '備注信息',
`update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存數(shù)據(jù)時(shí)間,自動(dòng)生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的資源';
resource_name 鎖資源名稱(chēng)必須有唯一索引。
2、使用姿勢(shì)
必須添加事務(wù),查詢(xún)和更新操作保證原子性,在一個(gè)事務(wù)里完成。
偽代碼實(shí)現(xiàn):
@Transaction
public void lock(String name) {
ResourceLock rlock=exeSql("select * from resource_lock where resource_name=name for update");
if (rlock==null) {
exeSql("insert into resource_lock(reosurce_name,owner,count) values (name, 'ip',0)");
}
}
使用 for update 鎖定的資源。
如果執(zhí)行成功,會(huì)立即返回,執(zhí)行插入數(shù)據(jù)庫(kù),后續(xù)再執(zhí)行一些其他業(yè)務(wù)邏輯,直到事務(wù)提交,執(zhí)行結(jié)束;
如果執(zhí)行失敗,就會(huì)一直阻塞著。
你也可以在數(shù)據(jù)庫(kù)客戶(hù)端工具上測(cè)試出來(lái)這個(gè)效果,當(dāng)在一個(gè)終端執(zhí)行了 for update,不提交事務(wù)。
在另外的終端上執(zhí)行相同條件的 for update,會(huì)一直卡著,轉(zhuǎn)圈圈...
雖然也能實(shí)現(xiàn)分布式鎖的效果,但是會(huì)存在性能瓶頸。
3、悲觀鎖優(yōu)缺點(diǎn)
優(yōu)點(diǎn):簡(jiǎn)單易用,好理解,保障數(shù)據(jù)強(qiáng)一致性。
缺點(diǎn)一大堆,羅列一下:
1)在 RR 事務(wù)級(jí)別,select 的 for update 操作是基于間隙鎖(gap lock) 實(shí)現(xiàn)的,是一種悲觀鎖的實(shí)現(xiàn)方式,所以存在阻塞問(wèn)題。
2)高并發(fā)情況下,大量請(qǐng)求進(jìn)來(lái),會(huì)導(dǎo)致大部分請(qǐng)求進(jìn)行排隊(duì),影響數(shù)據(jù)庫(kù)穩(wěn)定性,也會(huì)耗費(fèi)服務(wù)的CPU等資源。
當(dāng)獲得鎖的客戶(hù)端等待時(shí)間過(guò)長(zhǎng)時(shí),會(huì)提示:
[40001][1205] Lock wait timeout exceeded; try restarting transaction
高并發(fā)情況下,也會(huì)造成占用過(guò)多的應(yīng)用線程,導(dǎo)致業(yè)務(wù)無(wú)法正常響應(yīng)。
3)如果優(yōu)先獲得鎖的線程因?yàn)槟承┰?,一直沒(méi)有釋放掉鎖,可能會(huì)導(dǎo)致死鎖的發(fā)生。
4)鎖的長(zhǎng)時(shí)間不釋放,會(huì)一直占用數(shù)據(jù)庫(kù)連接,可能會(huì)將數(shù)據(jù)庫(kù)連接池?fù)伪绊懫渌?wù)。
6)不支持可重入特性,并且超時(shí)等待時(shí)間是全局的,不能隨便改動(dòng)。
樂(lè)觀鎖,以「樂(lè)觀的心態(tài)」來(lái)操作共享資源,無(wú)法獲得鎖成功,沒(méi)關(guān)系過(guò)一會(huì)重試一下看看唄,再不行就直接退出,嘗試一定次數(shù)還是不行?也可以以后再說(shuō),不用一直阻塞等著。
1、有一張資源表
為表添加一個(gè)字段,版本號(hào)或者時(shí)間戳都可以。通過(guò)版本號(hào)或者時(shí)間戳,來(lái)保證多線程同時(shí)間操作共享資源的有序性和正確性。
CREATE TABLE `resource` (
`id` int(4) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`resource_name` varchar(64) NOT NULL DEFAULT '' COMMENT '資源名',
`share` varchar(64) NOT NULL DEFAULT '' COMMENT '狀態(tài)',
`version` int(4) NOT NULL DEFAULT '' COMMENT '版本號(hào)',
`desc` varchar(1024) NOT NULL DEFAULT '備注信息',
`update_time` timestamp NOT NULL DEFAULT '' COMMENT '保存數(shù)據(jù)時(shí)間,自動(dòng)生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_resource_name` (`resource_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='資源';
2、使用姿勢(shì)
偽代碼實(shí)現(xiàn):
Resrouce resource=exeSql("select * from resource where resource_name=xxx");
boolean succ=exeSql("update resource set version='newVersion' ... where resource_name=xxx and version='oldVersion'");
if (!succ) {
// 發(fā)起重試
}
實(shí)際代碼中可以寫(xiě)個(gè)while循環(huán)不斷重試,版本號(hào)不一致,更新失敗,重新獲取新的版本號(hào),直到更新成功。
3、樂(lè)觀鎖優(yōu)缺點(diǎn)
優(yōu)點(diǎn):簡(jiǎn)單易用,保障數(shù)據(jù)一致性。
缺點(diǎn):
1)加行鎖的性能上有一定的開(kāi)銷(xiāo)
2)高并發(fā)場(chǎng)景下,線程內(nèi)的自旋操作 會(huì)耗費(fèi)一定的CPU資源。
另外,比如在更新數(shù)據(jù)狀態(tài)的一些場(chǎng)景下,不考慮冪等性的情況下,可以直接利用 行鎖 來(lái)保證數(shù)據(jù)一致性,示例:update table set state=1 where id=xxx and state=0;
樂(lè)觀鎖就類(lèi)似 CAS Compare And Swap 更新機(jī)制,推薦閱讀 <<一文徹底搞懂CAS>>
基于Redis實(shí)現(xiàn)的分布式鎖,性能上是最好的,實(shí)現(xiàn)上也是最復(fù)雜的。
前文中提到的 RedLock 是 Redis 之父 Antirez 提出來(lái)的分布式鎖的一種 「健壯」 的實(shí)現(xiàn)算法,但爭(zhēng)議也較多,一般不推薦使用。
Redis 2.6.12 之前的版本中采用 setnx + expire 方式實(shí)現(xiàn)分布式鎖,示例代碼如下所示:
public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result=jedis.setnx(lockKey, requestId);
//設(shè)置鎖
if (result==1) {
//獲取鎖成功
//若在這里程序突然崩潰,則無(wú)法設(shè)置過(guò)期時(shí)間,將發(fā)生死鎖
//通過(guò)過(guò)期時(shí)間刪除鎖
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
如果 lockKey 存在,則返回失敗,否則返回成功。設(shè)置成功之后,為了能在完成同步代碼之后成功釋放鎖,方法中使用 expire() 方法給 lockKey 設(shè)置一個(gè)過(guò)期時(shí)間,確認(rèn) key 值刪除,避免出現(xiàn)鎖無(wú)法釋放,導(dǎo)致下一個(gè)線程無(wú)法獲取到鎖,即死鎖問(wèn)題。
但是 setnx + expire 兩個(gè)命令放在程序里執(zhí)行,不是原子操作,容易出事。
如果程序設(shè)置鎖之后,此時(shí),在設(shè)置過(guò)期時(shí)間之前,程序崩潰了,如果 lockKey 沒(méi)有設(shè)置上過(guò)期時(shí)間,將會(huì)出現(xiàn)死鎖問(wèn)題。
解決以上問(wèn)題 ,有兩個(gè)辦法:
1)方式一:lua腳本
我們也可以通過(guò) Lua 腳本來(lái)實(shí)現(xiàn)鎖的設(shè)置和過(guò)期時(shí)間的原子性,再通過(guò) jedis.eval() 方法運(yùn)行該腳本:
// 加鎖腳本,KEYS[1] 要加鎖的key,ARGV[1]是UUID隨機(jī)值,ARGV[2]是過(guò)期時(shí)間
private static final String SCRIPT_LOCK="if redis.call('setnx', KEYS[1], ARGV[1])==1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
// 解鎖腳本,KEYS[1]要解鎖的key,ARGV[1]是UUID隨機(jī)值
private static final String SCRIPT_UNLOCK="if redis.call('get', KEYS[1])==ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
2)方式二:set原生命令
在 Redis 2.6.12 版本后 SETNX 增加了過(guò)期時(shí)間參數(shù):
SET lockKey anystring NX PX max-lock-time
程序?qū)崿F(xiàn)代碼如下:
public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result=jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
}
雖然 SETNX 方式能夠保證設(shè)置鎖和過(guò)期時(shí)間的原子性,但是如果我們?cè)O(shè)置的過(guò)期時(shí)間比較短,而執(zhí)行業(yè)務(wù)時(shí)間比較長(zhǎng),就會(huì)存在鎖代碼塊失效的問(wèn)題,失效后其他客戶(hù)端也能獲取到同樣的鎖,執(zhí)行同樣的業(yè)務(wù),此時(shí)可能就會(huì)出現(xiàn)一些問(wèn)題。
我們需要將過(guò)期時(shí)間設(shè)置得足夠長(zhǎng),來(lái)保證以上問(wèn)題不會(huì)出現(xiàn),但是設(shè)置多長(zhǎng)時(shí)間合理,也需要依具體業(yè)務(wù)來(lái)權(quán)衡。如果其他客戶(hù)端必須要阻塞拿到鎖,需要設(shè)計(jì)循環(huán)超時(shí)等待機(jī)制等問(wèn)題,感覺(jué)還挺麻煩的是吧。
除了使用Jedis客戶(hù)端之外,完全可以直接用Spring官方提供的企業(yè)集成模式框架,里面提供了很多分布式鎖的方式,Spring提供了一個(gè)統(tǒng)一的分布式鎖抽象,具體實(shí)現(xiàn)目前支持:
早期,分布式鎖的相關(guān)代碼存在于Spring Cloud的子項(xiàng)目Spring Cloud Cluster中,后來(lái)被遷到Spring Integration中。
Spring Integration 項(xiàng)目地址 :https://github.com/spring-projects/spring-integration
Spring強(qiáng)大之處在于此,對(duì)Lock分布式鎖做了全局抽象。
抽象結(jié)構(gòu)如下所示:
LockRegistry 作為頂層抽象接口:
/**
* Strategy for maintaining a registry of shared locks
*
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.1.1
*/
@FunctionalInterface
public interface LockRegistry {
/**
* Obtains the lock associated with the parameter object.
* @param lockKey The object with which the lock is associated.
* @return The associated lock.
*/
Lock obtain(Object lockKey);
}
定義的 obtain() 方法獲得具體的 Lock 實(shí)現(xiàn)類(lèi),分別在對(duì)應(yīng)的 XxxLockRegitry 實(shí)現(xiàn)類(lèi)來(lái)創(chuàng)建。
RedisLockRegistry 里obtain()方法實(shí)現(xiàn)類(lèi)為 RedisLock,RedisLock內(nèi)部,在Springboot2.x(Spring5)版本中是通過(guò)SET + PEXIPRE 命令結(jié)合lua腳本實(shí)現(xiàn)的,在Springboot1.x(Spring4)版本中,是通過(guò)SETNX命令實(shí)現(xiàn)的。
ZookeeperLockRegistry 里obtain()方法實(shí)現(xiàn)類(lèi)為 ZkLock,ZkLock內(nèi)部基于 Apache Curator 框架實(shí)現(xiàn)的。
JdbcLockRegistry 里obtain()方法實(shí)現(xiàn)類(lèi)為 JdbcLock,JdbcLock內(nèi)部基于一張INT_LOCK數(shù)據(jù)庫(kù)鎖表實(shí)現(xiàn)的,通過(guò)JdbcTemplate來(lái)操作。
客戶(hù)端使用方法:
private final String registryKey="sb2";
RedisLockRegistry lockRegistry=new RedisLockRegistry(getConnectionFactory(), this.registryKey);
Lock lock=lockRegistry.obtain("foo");
lock.lock();
try {
// doSth...
}
finally {
lock.unlock();
}
}
下面以目前最新版本的實(shí)現(xiàn),說(shuō)明加鎖和解鎖的具體過(guò)程。
RedisLockRegistry$RedisLock類(lèi)lock()加鎖流程:
加鎖步驟:
1)lockKey為registryKey:path,本例中為sb2:foo,客戶(hù)端C1優(yōu)先申請(qǐng)加鎖。
2)執(zhí)行l(wèi)ua腳本,get lockKey不存在,則set lockKey成功,值為clientid(UUID),過(guò)期時(shí)間默認(rèn)60秒。
3)客戶(hù)端C1同一個(gè)線程重復(fù)加鎖,pexpire lockKey,重置過(guò)期時(shí)間為60秒。
4)客戶(hù)端C2申請(qǐng)加鎖,執(zhí)行l(wèi)ua腳本,get lockKey已存在,并且跟已加鎖的clientid不同,加鎖失敗
5)客戶(hù)端C2掛起,每隔100ms再次嘗試加鎖。
RedisLock#lock()加鎖源碼實(shí)現(xiàn):
大家可以對(duì)照上面的流程圖配合你理解。
@Override
public void lock() {
this.localLock.lock();
while (true) {
try {
while (!obtainLock()) {
Thread.sleep(100); //NOSONAR
}
break;
}
catch (InterruptedException e) {
/*
* This method must be uninterruptible so catch and ignore
* interrupts and only break out of the while loop when
* we get the lock.
*/
}
catch (Exception e) {
this.localLock.unlock();
rethrowAsLockException(e);
}
}
}
// 基于Spring封裝的RedisTemplate來(lái)操作的
private boolean obtainLock() {
Boolean success= RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript,
Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId,
String.valueOf(RedisLockRegistry.this.expireAfter));
boolean result=Boolean.TRUE.equals(success);
if (result) {
this.lockedAt=System.currentTimeMillis();
}
return result;
}
執(zhí)行的lua腳本代碼:
private static final String OBTAIN_LOCK_SCRIPT="local lockClientId=redis.call('GET', KEYS[1])\n" +
"if lockClientId==ARGV[1] then\n" +
" redis.call('PEXPIRE', KEYS[1], ARGV[2])\n" +
" return true\n" +
"elseif not lockClientId then\n" +
" redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])\n" +
" return true\n" +
"end\n" +
"return false";
RedisLockRegistry$RedisLock類(lèi)unlock()解鎖流程:
RedisLock#unlock()源碼實(shí)現(xiàn):
@Override
public void unlock() {
if (!this.localLock.isHeldByCurrentThread()) {
throw new IllegalStateException("You do not own lock at " + this.lockKey);
}
if (this.localLock.getHoldCount() > 1) {
this.localLock.unlock();
return;
}
try {
if (!isAcquiredInThisProcess()) {
throw new IllegalStateException("Lock was released in the store due to expiration. " +
"The integrity of data protected by this lock may have been compromised.");
}
if (Thread.currentThread().isInterrupted()) {
RedisLockRegistry.this.executor.execute(this::removeLockKey);
}
else {
removeLockKey();
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Released lock; " + this);
}
}
catch (Exception e) {
ReflectionUtils.rethrowRuntimeException(e);
}
finally {
this.localLock.unlock();
}
}
// 刪除緩存Key
private void removeLockKey() {
if (this.unlinkAvailable) {
try {
RedisLockRegistry.this.redisTemplate.unlink(this.lockKey);
}
catch (Exception ex) {
LOGGER.warn("The UNLINK command has failed (not supported on the Redis server?); " +
"falling back to the regular DELETE command", ex);
this.unlinkAvailable=false;
RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
}
}
else {
RedisLockRegistry.this.redisTemplate.delete(this.lockKey);
}
}
unlock()解鎖方法里發(fā)現(xiàn),并不是直接就調(diào)用Redis的DEL命令刪除Key,這也是在Springboot2.x版本中做的一個(gè)優(yōu)化,Redis4.0版本以上提供了UNLINK命令。
換句話說(shuō),最新版本分布式鎖實(shí)現(xiàn),要求是Redis4.0以上版本才能使用。
看下Redis官網(wǎng)給出的一段解釋?zhuān)?/p>
This command is very similar to DEL: it removes the specified keys.
Just like DEL a key is ignored if it does not exist. However the
command performs the actual memory reclaiming in a different thread,
so it is not blocking, while DEL is. This is where the command name
comes from: the command just unlinks the keys from the keyspace. The
actual removal will happen later asynchronously.
DEL始終在阻止模式下釋放值部分。但如果該值太大,如對(duì)于大型LIST或HASH的分配太多,它會(huì)長(zhǎng)時(shí)間阻止Redis,為了解決這個(gè)問(wèn)題,Redis實(shí)現(xiàn)了UNLINK命令,即「非阻塞」刪除。如果值很小,則DEL一般與UNLINK效率上差不多。
本質(zhì)上,這種加鎖方式還是使用的SETNX實(shí)現(xiàn)的,而且Spring只是做了一層薄薄的封裝,支持可重入加鎖,超時(shí)等待,可中斷加鎖。
但是有個(gè)問(wèn)題,鎖的過(guò)期時(shí)間不能靈活設(shè)置,客戶(hù)端初始化時(shí),創(chuàng)建RedisLockRegistry時(shí)允許設(shè)置,但是是全局的。
/**
* Constructs a lock registry with the supplied lock expiration.
* @param connectionFactory The connection factory.
* @param registryKey The key prefix for locks.
* @param expireAfter The expiration in milliseconds.
*/
public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey, long expireAfter) {
Assert.notNull(connectionFactory, "'connectionFactory' cannot be null");
Assert.notNull(registryKey, "'registryKey' cannot be null");
this.redisTemplate=new StringRedisTemplate(connectionFactory);
this.obtainLockScript=new DefaultRedisScript<>(OBTAIN_LOCK_SCRIPT, Boolean.class);
this.registryKey=registryKey;
this.expireAfter=expireAfter;
}
expireAfter參數(shù)是全局的,同樣會(huì)存在問(wèn)題,可能是鎖過(guò)期時(shí)間到了,但是業(yè)務(wù)還沒(méi)有處理完,這把鎖又被另外的客戶(hù)端獲得,進(jìn)而會(huì)導(dǎo)致一些其他問(wèn)題。
經(jīng)過(guò)對(duì)源碼的分析,其實(shí)我們也可以借鑒RedisLockRegistry實(shí)現(xiàn)的基礎(chǔ)上,自行封裝實(shí)現(xiàn)分布式鎖,比如:
1、允許支持按照不同的Key設(shè)置過(guò)期時(shí)間,而不是全局的?
2、當(dāng)業(yè)務(wù)沒(méi)有處理完成,當(dāng)前客戶(hù)端啟動(dòng)個(gè)定時(shí)任務(wù)探測(cè),自動(dòng)延長(zhǎng)過(guò)期時(shí)間?
自己實(shí)現(xiàn)?嫌麻煩?別急別急!業(yè)界已經(jīng)有現(xiàn)成的實(shí)現(xiàn)方案了,那就是 Redisson 框架!
從Redis主從架構(gòu)上來(lái)考慮,依然存在問(wèn)題。因?yàn)?Redis 集群數(shù)據(jù)同步到各個(gè)節(jié)點(diǎn)時(shí)是異步的,如果在 Master 節(jié)點(diǎn)獲取到鎖后,在沒(méi)有同步到其它節(jié)點(diǎn)時(shí),Master 節(jié)點(diǎn)崩潰了,此時(shí)新的 Master 節(jié)點(diǎn)依然可以獲取鎖,所以多個(gè)應(yīng)用服務(wù)可以同時(shí)獲取到鎖。
基于以上的考慮,Redis之父Antirez提出了一個(gè)RedLock算法。
RedLock算法實(shí)現(xiàn)過(guò)程分析:
假設(shè)Redis部署模式是Redis Cluster,總共有5個(gè)master節(jié)點(diǎn),通過(guò)以下步驟獲取一把鎖:
1)獲取當(dāng)前時(shí)間戳,單位是毫秒
2)輪流嘗試在每個(gè)master節(jié)點(diǎn)上創(chuàng)建鎖,過(guò)期時(shí)間設(shè)置較短,一般就幾十毫秒
3)嘗試在大多數(shù)節(jié)點(diǎn)上建立一個(gè)鎖,比如5個(gè)節(jié)點(diǎn)就要求是3個(gè)節(jié)點(diǎn)(n / 2 +1)
4)客戶(hù)端計(jì)算建立好鎖的時(shí)間,如果建立鎖的時(shí)間小于超時(shí)時(shí)間,就算建立成功了
5)要是鎖建立失敗了,那么就依次刪除這個(gè)鎖
6)只要有客戶(hù)端創(chuàng)建成功了分布式鎖,其他客戶(hù)端就得不斷輪詢(xún)?nèi)L試獲取鎖
以上過(guò)程前文也提到了,進(jìn)一步分析RedLock算法的實(shí)現(xiàn)依然可能存在問(wèn)題,也是Martain和Antirez兩位大佬爭(zhēng)論的焦點(diǎn)。
問(wèn)題1:節(jié)點(diǎn)崩潰重啟
節(jié)點(diǎn)崩潰重啟,會(huì)出現(xiàn)多個(gè)客戶(hù)端持有鎖。
假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A、B、 C、 D、 E。設(shè)想發(fā)生了如下的事件序列:
1)客戶(hù)端C1成功對(duì)Redis集群中A、B、C三個(gè)節(jié)點(diǎn)加鎖成功(但D和E沒(méi)有鎖?。?。
2)節(jié)點(diǎn)C Duang的一下,崩潰重啟了,但客戶(hù)端C1在節(jié)點(diǎn)C加鎖未持久化完,丟了。
3)節(jié)點(diǎn)C重啟后,客戶(hù)端C2成功對(duì)Redis集群中C、D、 E嘗試加鎖成功了。
這樣,悲劇了吧!客戶(hù)端C1和C2同時(shí)獲得了同一把分布式鎖。
為了應(yīng)對(duì)節(jié)點(diǎn)重啟引發(fā)的鎖失效問(wèn)題,Antirez提出了延遲重啟的概念,即一個(gè)節(jié)點(diǎn)崩潰后,先不立即重啟它,而是等待一段時(shí)間再重啟,等待的時(shí)間大于鎖的有效時(shí)間。
采用這種方式,這個(gè)節(jié)點(diǎn)在重啟前所參與的鎖都會(huì)過(guò)期,它在重啟后就不會(huì)對(duì)現(xiàn)有的鎖造成影響。
這其實(shí)也是通過(guò)人為補(bǔ)償措施,降低不一致發(fā)生的概率。
問(wèn)題2:時(shí)鐘跳躍
假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A、B、 C、 D、 E。設(shè)想發(fā)生了如下的事件序列:
1)客戶(hù)端C1成功對(duì)Redis集群中A、B、 C三個(gè)節(jié)點(diǎn)成功加鎖。但因網(wǎng)絡(luò)問(wèn)題,與D和E通信失敗。
2)節(jié)點(diǎn)C上的時(shí)鐘發(fā)生了向前跳躍,導(dǎo)致它上面維護(hù)的鎖快速過(guò)期。
3)客戶(hù)端C2對(duì)Redis集群中節(jié)點(diǎn)C、 D、 E成功加了同一把鎖。
此時(shí),又悲劇了吧!客戶(hù)端C1和C2同時(shí)都持有著同一把分布式鎖。
為了應(yīng)對(duì)時(shí)鐘跳躍引發(fā)的鎖失效問(wèn)題,Antirez提出了應(yīng)該禁止人為修改系統(tǒng)時(shí)間,使用一個(gè)不會(huì)進(jìn)行「跳躍式」調(diào)整系統(tǒng)時(shí)鐘的ntpd程序。這也是通過(guò)人為補(bǔ)償措施,降低不一致發(fā)生的概率。
但是...,RedLock算法并沒(méi)有解決,操作共享資源超時(shí),導(dǎo)致鎖失效的問(wèn)題。
存在這么大爭(zhēng)議的算法實(shí)現(xiàn),還是不推薦使用的。
一般情況下,本文鎖介紹的框架提供的分布式鎖實(shí)現(xiàn)已經(jīng)能滿(mǎn)足大部分需求了。
上述,我們對(duì)spring-integration-redis實(shí)現(xiàn)原理進(jìn)行了深入分析,還對(duì)RedLock存在爭(zhēng)議的問(wèn)題做了分析。
除此以外,我們還提到了spring-integration中集成了 Jdbc、Zookeeper、Gemfire實(shí)現(xiàn)的分布式鎖,Gemfire和Jdbc大家感興趣可以自行去看下。
為啥還要提供個(gè)Jdbc分布式鎖實(shí)現(xiàn)?
猜測(cè)一下,當(dāng)你的應(yīng)用并發(fā)量也不高,比如是個(gè)后臺(tái)業(yè)務(wù),而且還沒(méi)依賴(lài)Zookeeper、Redis等額外的組件,只依賴(lài)了數(shù)據(jù)庫(kù)。
但你還想用分布式鎖搞點(diǎn)事兒,那好辦,直接用spring-integration-jdbc即可,內(nèi)部也是基于數(shù)據(jù)庫(kù)行鎖來(lái)實(shí)現(xiàn)的,需要你提前建好鎖表,創(chuàng)建表的SQL長(zhǎng)這樣:
CREATE TABLE INT_LOCK (
LOCK_KEY CHAR(36) NOT NULL,
REGION VARCHAR(100) NOT NULL,
CLIENT_ID CHAR(36),
CREATED_DATE DATETIME(6) NOT NULL,
constraint INT_LOCK_PK primary key (LOCK_KEY, REGION)
) ENGINE=InnoDB;
具體實(shí)現(xiàn)邏輯也非常簡(jiǎn)單,大家自己去看吧。
集成的Zookeeper實(shí)現(xiàn)的分布式鎖,因?yàn)槭腔贑urator框架實(shí)現(xiàn)的,不在本節(jié)展開(kāi),后續(xù)會(huì)有分析。
Redisson 是 Redis 的 Java 實(shí)現(xiàn)的客戶(hù)端,其 API 提供了比較全面的 Redis 命令的支持。
Jedis 簡(jiǎn)單使用阻塞的 I/O 和 Redis 交互,Redission 通過(guò) Netty 支持非阻塞 I/O。
Redisson 封裝了鎖的實(shí)現(xiàn),讓我們像操作我們的本地 Lock 一樣去使用,除此之外還有對(duì)集合、對(duì)象、常用緩存框架等做了友好的封裝,易于使用。
截止目前,Github上 Star 數(shù)量為 11.8k,說(shuō)明該開(kāi)源項(xiàng)目值得關(guān)注和使用。
Redisson分布式鎖Github:
https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers
Redisson 可以便捷的支持多種Redis部署架構(gòu):
// Master-Slave配置
Config config=new Config();
MasterSlaveServersConfig serverConfig=config.useMasterSlaveServers()
.setMasterAddress("")
.addSlaveAddress("")
.setReadMode(ReadMode.SLAVE)
.setMasterConnectionPoolSize(maxActiveSize)
.setMasterConnectionMinimumIdleSize(maxIdleSize)
.setSlaveConnectionPoolSize(maxActiveSize)
.setSlaveConnectionMinimumIdleSize(maxIdleSize)
.setConnectTimeout(CONNECTION_TIMEOUT_MS) // 默認(rèn)10秒
.setTimeout(socketTimeout)
;
RedissonClient redisson=Redisson.create(config);
RLock lock=redisson.getLock("myLock");
// 獲得鎖
lock.lock();
// 等待10秒未獲得鎖,自動(dòng)釋放
lock.lock(10, TimeUnit.SECONDS);
// 等待鎖定時(shí)間不超過(guò)100秒
// 10秒后自動(dòng)釋放鎖
boolean res=lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
使用上非常簡(jiǎn)單,RedissonClient客戶(hù)端提供了眾多的接口實(shí)現(xiàn),支持可重入鎖、、公平鎖、讀寫(xiě)鎖、鎖超時(shí)、RedLock等都提供了完整實(shí)現(xiàn)。
lock()加鎖流程:
為了兼容老的版本,Redisson里都是通過(guò)lua腳本執(zhí)行Redis命令的,同時(shí)保證了原子性操作。
加鎖執(zhí)行的lua腳本:
Redis里的Hash散列結(jié)構(gòu)存儲(chǔ)的。
參數(shù)解釋?zhuān)?/p>
KEY[1]:要加鎖的Key名稱(chēng),比如示例中的myLock。
ARGV[1]:針對(duì)加鎖的Key設(shè)置的過(guò)期時(shí)間
ARGV[2]:Hash結(jié)構(gòu)中Key名稱(chēng),lockName為UUID:線程ID
protected String getLockName(long threadId) {
return id + ":" + threadId;
}
1)客戶(hù)端C1申請(qǐng)加鎖,key為myLock。
2)如果key不存在,通過(guò)hset設(shè)置值,通過(guò)pexpire設(shè)置過(guò)期時(shí)間。同時(shí)開(kāi)啟Watchdog任務(wù),默認(rèn)每隔10秒中判斷一下,如果key還在,重置過(guò)期時(shí)間到30秒。
開(kāi)啟WatchDog源碼:
3)客戶(hù)端C1相同線程再次加鎖,如果key存在,判斷Redis里Hash中的lockName跟當(dāng)前線程lockName相同,則將Hash中的lockName的值加1,代表支持可重入加鎖。
4)客戶(hù)單C2申請(qǐng)加鎖,如果key存在,判斷Redis里Hash中的lockName跟當(dāng)前線程lockName不同,則執(zhí)行pttl返回剩余過(guò)期時(shí)間。
5)客戶(hù)端C2線程內(nèi)不斷嘗試pttl時(shí)間,此處是基于Semaphore信號(hào)量實(shí)現(xiàn)的,有許可立即返回,否則等到pttl時(shí)間還是沒(méi)有得到許可,繼續(xù)重試。
重試源碼:
Redisson這樣的實(shí)現(xiàn)就解決了,當(dāng)業(yè)務(wù)處理時(shí)間比過(guò)期時(shí)間長(zhǎng)的問(wèn)題。
同時(shí),Redisson 還自己擴(kuò)展 Lock 接口,叫做 RLock 接口,擴(kuò)展了很多的鎖接口,比如給 Key 設(shè)定過(guò)期時(shí)間,非阻塞+超時(shí)時(shí)間等。
void lock(long leaseTime, TimeUnit unit);
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
redisson里的WatchDog(看門(mén)狗)邏輯保證了沒(méi)有死鎖發(fā)生。
如果客戶(hù)端宕機(jī)了,WatchDog任務(wù)也就跟著停掉了。此時(shí),不會(huì)對(duì)Key重置過(guò)期時(shí)間了,等掛掉的客戶(hù)端持有的Key過(guò)期時(shí)間到了,鎖自動(dòng)釋放,其他客戶(hù)端嘗試獲得這把鎖。
可以進(jìn)一步看官網(wǎng)的關(guān)于WatchDog描述:
If Redisson instance which acquired lock crashes then such lock could hang forever in acquired state. To avoid this Redisson maintains lock watchdog, it prolongs lock expiration while lock holder Redisson instance is alive. By default lock watchdog timeout is 30 seconds and can be changed through Config.lockWatchdogTimeout setting.
unlock()解鎖過(guò)程也是同樣的,通過(guò)lua腳本執(zhí)行一大坨指令的。
解鎖lua腳本:
解鎖lua腳本
根據(jù)剛剛對(duì)加鎖過(guò)程的分析,大家可以自行看下腳本分析下。
Zookeeper 是一種提供「分布式服務(wù)協(xié)調(diào)」的中心化服務(wù),是以 Paxos 算法為基礎(chǔ)實(shí)現(xiàn)的。Zookeeper數(shù)據(jù)節(jié)點(diǎn)和文件目錄類(lèi)似,同時(shí)具有Watch機(jī)制,基于這兩個(gè)特性,得以實(shí)現(xiàn)分布式鎖功能。
數(shù)據(jù)節(jié)點(diǎn):
順序臨時(shí)節(jié)點(diǎn):Zookeeper 提供一個(gè)多層級(jí)的節(jié)點(diǎn)命名空間(節(jié)點(diǎn)稱(chēng)為 Znode),每個(gè)節(jié)點(diǎn)都用一個(gè)以斜杠(/)分隔的路徑來(lái)表示,而且每個(gè)節(jié)點(diǎn)都有父節(jié)點(diǎn)(根節(jié)點(diǎn)除外),非常類(lèi)似于文件系統(tǒng)。
節(jié)點(diǎn)類(lèi)型可以分為持久節(jié)點(diǎn)(PERSISTENT )、臨時(shí)節(jié)點(diǎn)(EPHEMERAL),每個(gè)節(jié)點(diǎn)還能被標(biāo)記為有序性(SEQUENTIAL),一旦節(jié)點(diǎn)被標(biāo)記為有序性,那么整個(gè)節(jié)點(diǎn)就具有順序自增的特點(diǎn)。
一般我們可以組合這幾類(lèi)節(jié)點(diǎn)來(lái)創(chuàng)建我們所需要的節(jié)點(diǎn),例如,創(chuàng)建一個(gè)持久節(jié)點(diǎn)作為父節(jié)點(diǎn),在父節(jié)點(diǎn)下面創(chuàng)建臨時(shí)節(jié)點(diǎn),并標(biāo)記該臨時(shí)節(jié)點(diǎn)為有序性。
Watch 機(jī)制:
Zookeeper 還提供了另外一個(gè)重要的特性,Watcher(事件監(jiān)聽(tīng)器)。
ZooKeeper 允許用戶(hù)在指定節(jié)點(diǎn)上注冊(cè)一些 Watcher,并且在一些特定事件觸發(fā)的時(shí)候,ZooKeeper 服務(wù)端會(huì)將事件通知給用戶(hù)。
圖解Zookeeper實(shí)現(xiàn)分布式鎖:
zk臨時(shí)順序節(jié)點(diǎn)機(jī)制
首先,我們需要建立一個(gè)父節(jié)點(diǎn),節(jié)點(diǎn)類(lèi)型為持久節(jié)點(diǎn)(PERSISTENT)如圖中的 /locks/lock_name1 節(jié)點(diǎn) ,每當(dāng)需要訪問(wèn)共享資源時(shí),就會(huì)在父節(jié)點(diǎn)下建立相應(yīng)的順序子節(jié)點(diǎn),節(jié)點(diǎn)類(lèi)型為臨時(shí)節(jié)點(diǎn)(EPHEMERAL),且標(biāo)記為有序性(SEQUENTIAL),并且以臨時(shí)節(jié)點(diǎn)名稱(chēng) + 父節(jié)點(diǎn)名稱(chēng) + 順序號(hào)組成特定的名字,如圖中的 /0000000001 /0000000002 /0000000003 作為臨時(shí)有序節(jié)點(diǎn)。
在建立子節(jié)點(diǎn)后,對(duì)父節(jié)點(diǎn)下面的所有以臨時(shí)節(jié)點(diǎn)名稱(chēng) name 開(kāi)頭的子節(jié)點(diǎn)進(jìn)行排序,判斷剛剛建立的子節(jié)點(diǎn)順序號(hào)是否是最小的節(jié)點(diǎn),如果是最小節(jié)點(diǎn),則獲得鎖。
如果不是最小節(jié)點(diǎn),則阻塞等待鎖,并且獲得該節(jié)點(diǎn)的上一順序節(jié)點(diǎn),為其注冊(cè)監(jiān)聽(tīng)事件,等待節(jié)點(diǎn)對(duì)應(yīng)的操作獲得鎖。當(dāng)調(diào)用完共享資源后,刪除該節(jié)點(diǎn),關(guān)閉 zk,進(jìn)而可以觸發(fā)監(jiān)聽(tīng)事件,釋放該鎖。
// 加鎖
InterProcessMutex lock=new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) )
{
try
{
// do some work inside of the critical section here
}
finally
{
lock.release();
}
}
public void acquire() throws Exception
{
if ( !internalLock(-1, null) )
{
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
/*
Note on concurrency: a given lockData instance
can be only acted on by a single thread so locking isn't necessary
*/
Thread currentThread=Thread.currentThread();
LockData lockData=threadData.get(currentThread);
if ( lockData !=null )
{
// re-entering
lockData.lockCount.incrementAndGet();
return true;
}
String lockPath=internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath !=null )
{
LockData newLockData=new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
// ... 其他代碼略
InterProcessMutex 是 Curator 實(shí)現(xiàn)的可重入鎖,可重入鎖源碼過(guò)程分析:
加鎖流程:
1)可重入鎖記錄在 ConcurrentMap<Thread, LockData> threadData 這個(gè) Map 里面。
2)如果 threadData.get(currentThread) 是有值的那么就證明是可重入鎖,然后記錄就會(huì)加 1。
3)資源目錄下創(chuàng)建一個(gè)節(jié)點(diǎn):比如這里創(chuàng)建一個(gè) /0000000002 這個(gè)節(jié)點(diǎn),這個(gè)節(jié)點(diǎn)需要設(shè)置為 EPHEMERAL_SEQUENTIAL 也就是臨時(shí)節(jié)點(diǎn)并且有序。
4)獲取當(dāng)前目錄下所有子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)是否是最小的節(jié)點(diǎn)。
5)如果是最小的節(jié)點(diǎn),則獲取到鎖。如果不是最小的節(jié)點(diǎn),則證明前面已經(jīng)有人獲取到鎖了,那么需要獲取自己節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)。
6)節(jié)點(diǎn) /0000000002 的前一個(gè)節(jié)點(diǎn)是 /0000000001,我們獲取到這個(gè)節(jié)點(diǎn)之后,再上面注冊(cè) Watcher,Watcher 調(diào)用的是 object.notifyAll(),用來(lái)解除阻塞。
7)object.wait(timeout) 或 object.wait() 進(jìn)行阻塞等待
解鎖流程:
1)如果可重入鎖次數(shù)減1后,加鎖次數(shù)不為 0 直接返回,減1后加鎖次數(shù)為0,繼續(xù)。
2)刪除當(dāng)前節(jié)點(diǎn)。
3)刪除 threadDataMap 里面的可重入鎖的數(shù)據(jù)。
上面介紹的諸如Apache Curator、Redisson、Spring框架集成的分布式鎖,既然是框架實(shí)現(xiàn),會(huì)考慮用戶(hù)需求,盡量設(shè)計(jì)和實(shí)現(xiàn)通用的分布式鎖接口。
基本都涵蓋了如下的方式實(shí)現(xiàn):
分布式鎖實(shí)現(xiàn)接口
當(dāng)然,Redisson和Curator都是自己定義的分布式鎖接口實(shí)現(xiàn)的,易于擴(kuò)展。
Curator里自定義了InterProcessLock接口,Redisson里自定義RLock接口,繼承了 java.util.concurrent.locks.Lock接口。
對(duì)于Redis實(shí)現(xiàn)的分布式鎖:
大部分需求下,不會(huì)遇到「極端復(fù)雜場(chǎng)景」,基于Redis實(shí)現(xiàn)分布式鎖很常用,性能也高。
它獲取鎖的方式簡(jiǎn)單粗暴,獲取不到鎖直接不斷嘗試獲取鎖,比較消耗性能。
另外來(lái)說(shuō)的話,redis的設(shè)計(jì)定位決定了它的數(shù)據(jù)并不是強(qiáng)一致性的,沒(méi)有一致性算法,在某些極端情況下,可能會(huì)出現(xiàn)問(wèn)題,鎖的模型不夠健壯。
即便有了Redlock算法的實(shí)現(xiàn),但存在爭(zhēng)議,某些復(fù)雜場(chǎng)景下,也無(wú)法保證其實(shí)現(xiàn)完全沒(méi)有問(wèn)題,并且也是比較消耗性能的。
對(duì)于Zookeeper實(shí)現(xiàn)的分布式鎖:
Zookeeper優(yōu)點(diǎn):
天生設(shè)計(jì)定位是分布式協(xié)調(diào),強(qiáng)一致性。鎖的模型健壯、簡(jiǎn)單易用、適合做分布式鎖。
如果獲取不到鎖,只需要添加一個(gè)監(jiān)聽(tīng)器就可以了,不用一直輪詢(xún),性能消耗較小。
如果客戶(hù)端宕機(jī),也沒(méi)關(guān)系,臨時(shí)節(jié)點(diǎn)會(huì)自動(dòng)刪除,觸發(fā)監(jiān)聽(tīng)器通知下一個(gè)節(jié)點(diǎn)。
Zookeeper缺點(diǎn):
若有大量的客戶(hù)端頻繁的申請(qǐng)加鎖、釋放鎖,對(duì)于ZK集群的壓力會(huì)比較大。
另外,本文對(duì)spring-integration集成redis做了詳細(xì)分析,推薦可以直接使用,更推薦直接使用 Redisson,實(shí)現(xiàn)了非常多的分布式鎖各種機(jī)制,有單獨(dú)開(kāi)放Springboot集成的jar包,使用上也是非常方便的。
文章開(kāi)頭部分提到的幾個(gè)業(yè)務(wù)場(chǎng)景,經(jīng)過(guò)對(duì)分布式鎖家族的介紹和原理分析,可以自行選擇技術(shù)方案了。
以上,一定有一款能滿(mǎn)足你的需求,希望大家有所收獲!
章我們就來(lái)說(shuō)說(shuō)網(wǎng)頁(yè)中的交互必備屬性, 表單 form, 這個(gè)是頁(yè)面交互用戶(hù)錄入的關(guān)鍵知識(shí)點(diǎn), 在 Vue.js 中使用指令 v-model 進(jìn)行數(shù)據(jù)的雙向綁定。它會(huì)根據(jù)控件類(lèi)型自動(dòng)的更新數(shù)據(jù)到頁(yè)面中。需要注意的是, 在數(shù)據(jù)初始化的時(shí)候, v-model 會(huì)忽略頁(yè)面中自帶有的屬性值, 只會(huì)使用組件中的 data 函數(shù)進(jìn)行數(shù)據(jù)的初始化。
Form 表單包含的元素有:
我們從最常用且最簡(jiǎn)單的 文本內(nèi)容說(shuō)起??纯?Vue.js 給我們帶來(lái)了什么便利的改變。
input 默認(rèn)類(lèi)型為: text, 并且使用 v-model 進(jìn)行數(shù)據(jù)雙向綁定, 直接查看代碼:
運(yùn)行 F5 調(diào)試, 瀏覽器查看效果:
這就是雙向綁定的好處, 如果沒(méi)有這個(gè)雙向綁定之前, 你需要先綁定 2 個(gè)事件, 一個(gè)是表單的 change 事件, 一個(gè)是元素的更新時(shí)間, 在 change 事件觸發(fā)的時(shí)候, 使用 innerText 更新頁(yè)面元素的內(nèi)容。
查看案例:
調(diào)試查看頁(yè)面, 在第二個(gè)文本域中寫(xiě)入文字, 你會(huì)發(fā)現(xiàn)所有的文本域也會(huì)跟著改變, 但是這個(gè)不是最好的方式, 如果修改了第一個(gè)文本域的值, 你會(huì)發(fā)現(xiàn), 后面 v-model 綁定的數(shù)據(jù)并沒(méi)有變化 查看效果:
最好的方式就是使用: <textarea v-model="userName"></textarea>
復(fù)選框允許你為表單提交時(shí)選擇一個(gè)值或者一組值。單個(gè)復(fù)選框, 可以綁定到布爾值類(lèi)型,舉例如下:
調(diào)試查看未選中:
選中之后的效果:
在多個(gè)復(fù)選框的情況下, 可以將 input 值綁定到一個(gè)數(shù)組中, 代碼如下:
重新打開(kāi)該頁(yè)面, 進(jìn)行查看效果。 選擇之后。 就可以看到效果。這里只是為了演示, 實(shí)際中的 value 都是標(biāo)志字符串。
在復(fù)選框中, 可以使用 true-value 和 false-value 進(jìn)行選中和未選中的值的賦值。
<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
// 選中的時(shí)候值就為 yes:
vm.toggle==='yes'
// 未選中的時(shí)候值就為 no:
vm.toggle==='no'
單選框按鈕允許你選擇單一的值來(lái)提交表單。 默認(rèn)的情況下為小型圓圈圖表。一個(gè)單選按鈕由具有相同 name 屬性的單選按鈕組成的。舉個(gè)例子:
打開(kāi)瀏覽器查看效果:
HTML <select> 元素是一個(gè)提供選項(xiàng)的菜單控件。每個(gè) select 都包含 option 子元素, 并且 option子元素中包含 value 屬性, 用于標(biāo)識(shí)當(dāng)前的會(huì)提交的數(shù)據(jù)值。還可以將 option 放到 optgroup 中, 標(biāo)識(shí)不同的組別。 并且可以在 select 屬性上使用 multiple 決定該選擇框是否可以多選。查看單選例子:
調(diào)試查看效果:
如果選擇了 甲 就可以看到下面的效果。
看 乙 的代碼重新指定了 value 值。 所以看到效果如下:
只需要指定 select 的 multiple 屬性。
查看效果, 這里需要注意, 使用 ctrl 按下不松進(jìn)行鼠標(biāo)單擊進(jìn)行多項(xiàng)選擇:
從上面我們可以看到我們的數(shù)據(jù)基本上就是一個(gè)數(shù)組項(xiàng), 這里既然是個(gè)數(shù)組, 就可以使用 v-for 標(biāo)簽進(jìn)行渲染, 在實(shí)際開(kāi)發(fā)的過(guò)程中, 也多數(shù)都是使用 v-for 進(jìn)行渲染。 舉個(gè)例子:
查看效果:
前面在說(shuō)事件的時(shí)候, 說(shuō)過(guò)事件的修飾符, 在表單中也有修飾符。 分別是: .lazy, .number, .trim
在默認(rèn)渲染的情況下,v-model 在每次 input 事件觸發(fā)后都將輸入的值數(shù)據(jù)進(jìn)行雙向綁定, 數(shù)據(jù)更新, 如果不想要這種效果的時(shí)候, 可以嘗試使用 lazy 修飾符, 這樣數(shù)據(jù)就只會(huì)在 change 事件觸發(fā)之后, 數(shù)據(jù)才會(huì)修改。
這個(gè)時(shí)候, 不觸發(fā) change 事件就不會(huì)更新數(shù)據(jù)到頁(yè)面上。
將用戶(hù)輸入的數(shù)據(jù)轉(zhuǎn)換成數(shù)值類(lèi)型。但是需要注意的是, 必須是能夠正常轉(zhuǎn)換的字符串才有效, 負(fù)責(zé)只會(huì)返回原始值。
<input v-model.number="age" type="number" />
這個(gè)就是很容易理解了, 就是給字符串去除首尾的空白字符。
<input v-model.trim="msg" />
表單的相關(guān)內(nèi)容這里暫時(shí)告一段落。 后續(xù)會(huì)針對(duì)這塊進(jìn)行源碼級(jí)別分析。 關(guān)注我。 看后續(xù)的內(nèi)容。
每天都強(qiáng)制自己做點(diǎn)非舒服區(qū)的知識(shí)學(xué)習(xí), 你會(huì)發(fā)現(xiàn)你進(jìn)步神速的。加油!
過(guò)一段時(shí)間公測(cè),得到廣大客戶(hù)的熱烈支持,阿里云時(shí)空數(shù)據(jù)庫(kù)已經(jīng)于2019年9月10日正式商業(yè)化售賣(mài)!
時(shí)空數(shù)據(jù)庫(kù)能夠存儲(chǔ)、管理包括時(shí)間序列以及空間地理位置相關(guān)的數(shù)據(jù)。我們的社會(huì)生產(chǎn)、經(jīng)濟(jì)活動(dòng)和社會(huì)交往同時(shí)空數(shù)據(jù)密切相關(guān),比如傳感器網(wǎng)絡(luò)、移動(dòng)互聯(lián)網(wǎng)、射頻識(shí)別、全球定位系統(tǒng)等設(shè)備時(shí)刻輸出時(shí)間和空間數(shù)據(jù),數(shù)據(jù)量增長(zhǎng)非常迅速,這對(duì)存儲(chǔ)和管理時(shí)空數(shù)據(jù)帶來(lái)了挑戰(zhàn),傳統(tǒng)數(shù)據(jù)庫(kù)很難應(yīng)對(duì)時(shí)空數(shù)據(jù)。時(shí)空數(shù)據(jù)是一種高維數(shù)據(jù),普通的關(guān)系型數(shù)據(jù)庫(kù)更適合于存儲(chǔ)數(shù)值和字符類(lèi)型數(shù)據(jù),也缺少相關(guān)的算子。阿里云時(shí)空數(shù)據(jù)庫(kù)具有時(shí)空數(shù)據(jù)模型、時(shí)空索引和時(shí)空算子,完全兼容SQL及SQL/MM標(biāo)準(zhǔn),支持時(shí)空數(shù)據(jù)同業(yè)務(wù)數(shù)據(jù)一體化存儲(chǔ)、無(wú)縫銜接,易于集成使用。
產(chǎn)品首頁(yè):https://www.aliyun.com/product/hitsdb_spatialpre
產(chǎn)品使用手冊(cè)詳見(jiàn):https://help.aliyun.com/document_detail/116088.html?spm=a2c4g.11174283.6.727.1b22130eu4OBeh
交通監(jiān)控與分析、物流配送、可穿戴設(shè)備監(jiān)測(cè)、新能源車(chē)輛監(jiān)測(cè)、LBS、地圖服務(wù)等。
在購(gòu)買(mǎi)時(shí)空數(shù)據(jù)庫(kù)之前,需要先滿(mǎn)足以下前提條件:
以“華北1(杭州)” 地域?yàn)槔?,下面演示具體創(chuàng)建流程。
ACTION1: 在使用的區(qū)域內(nèi),創(chuàng)建VPC實(shí)例
https://vpc.console.aliyun.com/vpc/cn-hangzhou/vpcs
以“華北1(杭州)”區(qū)為例,選擇地域:華東1(杭州),交換機(jī)選項(xiàng)中,選擇需要的可用區(qū)如“杭州 可用區(qū)B”, 后續(xù)創(chuàng)建時(shí)空數(shù)據(jù)庫(kù)實(shí)例,會(huì)用到 “地域” 、“可用區(qū)”、“VPC”、“交換機(jī)” 這幾個(gè)概念。
創(chuàng)建VPC完成之后,可以查看VPC的詳情
ACTION2: 創(chuàng)建時(shí)空數(shù)據(jù)庫(kù)實(shí)例
https://common-buy.aliyun.com/?commodityCode=hitsdb_spatialpre#/buy
ACTION3: 購(gòu)買(mǎi)成功之后,登錄TSDB控制臺(tái),查看實(shí)例詳情
https://tsdb.console.aliyun.com/?spm=5176.11182172.console-base-top.dconsoleEntry.60ec4882eEzNPU#/cluster/cn-hangzhou
ACTION4: 在“實(shí)例詳情”頁(yè)面中,查看“公共網(wǎng)絡(luò)地址” 和 “VPC網(wǎng)絡(luò)地址”,設(shè)置網(wǎng)絡(luò)白名單。
這里為了測(cè)試方便,VPN和公共網(wǎng)絡(luò)的參數(shù)都設(shè)置成“0.0.0.0/0”
ACTION5: 在“實(shí)例詳情”頁(yè)面中,左側(cè)選擇“賬戶(hù)管理”進(jìn)入賬戶(hù)創(chuàng)建頁(yè)面,創(chuàng)建高權(quán)限賬號(hào)
至此,整個(gè)時(shí)空數(shù)據(jù)庫(kù)的初始化工作已經(jīng)完成,可以通過(guò)外部網(wǎng)絡(luò)或VPC專(zhuān)有網(wǎng)絡(luò),連接時(shí)空數(shù)據(jù)庫(kù)交互。
時(shí)空數(shù)據(jù)庫(kù)寫(xiě)入和查詢(xún)非常便利,讀寫(xiě)采用標(biāo)準(zhǔn)SQL,用戶(hù)可以通過(guò)JDBC/ODBC驅(qū)動(dòng)操作數(shù)據(jù)庫(kù),進(jìn)行讀寫(xiě)操作。
用戶(hù)也可以通過(guò)psql交互式終端向時(shí)空數(shù)據(jù)庫(kù)寫(xiě)入和查詢(xún)數(shù)據(jù),下面是幾個(gè)簡(jiǎn)單的例子:
創(chuàng)建一個(gè)時(shí)空表:
CREATE TABLE tsdb_test( uid bigint, time timestamp, speed float, position geometry(Point,4326) ); SELECT create_hypertable('tsdb_test', 'time', chunk_time_interval=> interval '1 hour');
寫(xiě)入數(shù)據(jù):
INSERT INTO tsdb_test VALUES (1001, '2019-03-11 16:34:15', 102.2, ST_SetSRID(ST_MakePoint(10.3,20.1),4326)), (1001, '2019-03-11 16:34:16', 100.1, ST_SetSRID(ST_MakePoint(10.4,20.1),4326)), (1002, '2019-03-11 16:34:17', 60.0, ST_SetSRID(ST_MakePoint(10.5,20.2),4326)), (1002, '2019-03-11 16:34:18', 61.0, ST_SetSRID(ST_MakePoint(10.6,20.2),4326)), (1003, '2019-03-11 16:34:20', 39.0, ST_SetSRID(ST_MakePoint(10.7,20.2),4326)), (1003, '2019-03-11 16:34:21', 30.0, ST_SetSRID(ST_MakePoint(10.8,20.2),4326));
用戶(hù)通過(guò)交互終端查詢(xún)數(shù)據(jù),可以如下:
SELECT time,uid,speed,ST_AsText(position) FROM tsdb_test WHERE time >'2019-03-11 16:00:00' AND time < '2019-03-11 18:00:00' AND ST_Contains(ST_SetSRID(ST_MakeBox2D(ST_Point(2.4, 5.5),ST_Point(13.0,26.1)),4326),position) ; +---------------------+---------------+-----------------+---------------------+ | TIME | UID | SPEED | ST_ASTEXT | +---------------------+---------------+-----------------+---------------------+ | 2019-03-11 16:34:15 | 1001 | 102.2 | POINT(10.3 20.1) | | 2019-03-11 16:34:16 | 1001 | 100.1 | POINT(10.4 20.1) | | 2019-03-11 16:34:17 | 1002 | 60 | POINT(10.5 20.2) | | 2019-03-11 16:34:18 | 1002 | 61 | POINT(10.6 20.2) | | 2019-03-11 16:34:20 | 1003 | 39 | POINT(10.7 20.2) | | 2019-03-11 16:34:21 | 1003 | 30 | POINT(10.8 20.2) | +---------------------+---------------+-----------------+---------------------+
更新數(shù)據(jù):
UPDATE tsdb_test set position=ST_SetSRID(ST_MakePoint(11.1,22.2),4326) WHERE uid=1002;
用戶(hù)可以使用時(shí)間&空間分析函數(shù),對(duì)時(shí)空數(shù)據(jù)庫(kù)中的表做分析查詢(xún)。以共享汽車(chē)平臺(tái)中車(chē)輛數(shù)據(jù)為背景,舉幾個(gè)簡(jiǎn)單的例子。
按時(shí)間窗口聚合
按照5分鐘為一個(gè)聚合時(shí)間窗口,獲取共享汽車(chē)平臺(tái)中車(chē)輛的最大速度;常見(jiàn)聚合函數(shù)如:sum,max,min,avg等
SELECT uid,time_bucket('5 minutes', time) AS interval, max(speed) FROM tsdb_test WHERE uid='1002' and time < '2019-04-01 01:13:42' GROUP BY uid, interval ORDER BY interval DESC; +---------------+---------------------+---------------+ | UID | INTERVAL | MAX | +---------------+---------------------+---------------+ | 1002 | 2019-03-11 16:30:00 | 61 | +---------------+---------------------+---------------+
按時(shí)間段和距離過(guò)濾
返回某個(gè)時(shí)間段,與指定對(duì)象的距離大于“17米”的車(chē)輛??臻g范圍函數(shù)比如: ST_Distance等使用,參考:空間對(duì)象關(guān)系函數(shù)。
SELECT time,uid,speed,ST_AsText(position) FROM tsdb_test WHERE time > '2019-01-01 01:02:00' and time < '2019-04-01 01:11:02' and ST_Distance('SRID=4326;POINT(2.4 5.5)'::geometry, position)>17.0; +---------------------+---------------+-----------------+---------------------+ | TIME | UID | SPEED | ST_ASTEXT | +---------------------+---------------+-----------------+---------------------+ | 2019-03-11 16:34:17 | 1002 | 60 | POINT(11.1 22.2) | | 2019-03-11 16:34:18 | 1002 | 61 | POINT(11.1 22.2) | +---------------------+---------------+-----------------+---------------------+
普通屬性值過(guò)濾
根據(jù)用戶(hù)設(shè)置的數(shù)值限制條件,返回某時(shí)間段內(nèi)“速度>60”的車(chē)輛記錄。比如: “>”, “<”, “=”, “<=”, “>=”, “!=”。ST_AsText的使用,參考:空間對(duì)象輸出函數(shù)
SELECT time,uid,speed,ST_AsText(position) FROM tsdb_test WHERE time > '2019-03-01 01:02:00' and time < '2019-03-15 01:11:02' and speed > 60; +---------------------+---------------+-----------------+---------------------+ | TIME | UID | SPEED | ST_ASTEXT | +---------------------+---------------+-----------------+---------------------+ | 2019-03-11 16:34:15 | 1001 | 102.2 | POINT(10.3 20.1) | | 2019-03-11 16:34:16 | 1001 | 100.1 | POINT(10.4 20.1) | | 2019-03-11 16:34:18 | 1002 | 61 | POINT(11.1 22.2) | +---------------------+---------------+-----------------+---------------------+
關(guān)于時(shí)空數(shù)據(jù)庫(kù)的具體用法,可以參考阿里云時(shí)空數(shù)據(jù)庫(kù)-開(kāi)發(fā)指南
阿里云時(shí)空數(shù)據(jù)庫(kù)致力于推動(dòng)時(shí)空領(lǐng)域生態(tài)發(fā)展,為客戶(hù)提供低成本高性能服務(wù),讓時(shí)空數(shù)據(jù)價(jià)值在線化!
產(chǎn)品首頁(yè):https://www.aliyun.com/product/hitsdb_spatialpre
產(chǎn)品使用手冊(cè)詳見(jiàn):https://help.aliyun.com/document_detail/116088.html?spm=a2c4g.11174283.6.727.1b22130eu4OBeh
時(shí)空數(shù)據(jù)庫(kù)實(shí)例創(chuàng)建:https://common-buy.aliyun.com/?commodityCode=hitsdb_spatialpre#/buy
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。