分布式鎖
我們?cè)陂_(kāi)發(fā)應(yīng)用的時(shí)候,如果需要對(duì)某一個(gè)共享變量進(jìn)行多線程同步訪問(wèn)的時(shí)候源譬,可以使用我們學(xué)到的鎖進(jìn)行處理集惋,并且可以完美的運(yùn)行,毫無(wú)Bug踩娘!
在傳統(tǒng)單體應(yīng)用單機(jī)部署的情況下刮刑,可以使用并發(fā)處理相關(guān)的功能進(jìn)行互斥控制。但是养渴,隨著業(yè)務(wù)發(fā)展的需要雷绢,原單體單機(jī)部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程理卑、多進(jìn)程并且分布在不同機(jī)器上翘紊,這將使原單機(jī)部署情況下的并發(fā)控制鎖策略失效,單純的應(yīng)用并不能提供分布式鎖的能力藐唠。為了解決這個(gè)問(wèn)題就需要一種跨機(jī)器的互斥機(jī)制來(lái)控制共享資源的訪問(wèn)帆疟,這就是分布式鎖要解決的問(wèn)題鹉究!
在分析分布式鎖的三種實(shí)現(xiàn)方式之前,先了解一下分布式鎖應(yīng)該具備哪些條件:
1踪宠、在分布式系統(tǒng)環(huán)境下自赔,一個(gè)方法在同一時(shí)間只能被一個(gè)機(jī)器的一個(gè)線程執(zhí)行;
2柳琢、高可用的獲取鎖與釋放鎖绍妨;
3、高性能的獲取鎖與釋放鎖柬脸;
4他去、具備可重入特性;
5倒堕、具備鎖失效機(jī)制灾测,防止死鎖;
6垦巴、具備阻塞鎖特性行施,即沒(méi)有獲取到鎖將直接返回獲取鎖失敗。
目前幾乎很多大型網(wǎng)站及應(yīng)用都是分布式部署的魂那,分布式場(chǎng)景中的數(shù)據(jù)一致性問(wèn)題一直是一個(gè)比較重要的話題蛾号。分布式的CAP理論告訴我們“任何一個(gè)分布式系統(tǒng)都無(wú)法同時(shí)滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯(cuò)性(Partition tolerance)涯雅,最多只能同時(shí)滿足兩項(xiàng)鲜结。”所以活逆,很多系統(tǒng)在設(shè)計(jì)之初就要對(duì)這三者做出取舍精刷。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場(chǎng)景中,都需要犧牲強(qiáng)一致性來(lái)?yè)Q取系統(tǒng)的高可用性蔗候,系統(tǒng)往往只需要保證“最終一致性”怒允,只要這個(gè)最終時(shí)間是在用戶可以接受的范圍內(nèi)即可。
在很多場(chǎng)景中锈遥,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性纫事,需要很多的技術(shù)方案來(lái)支持,比如分布式事務(wù)所灸、分布式鎖等丽惶。有的時(shí)候,我們需要保證一個(gè)方法在同一時(shí)間內(nèi)只能被同一個(gè)線程執(zhí)行爬立。
基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖钾唬;
基于緩存(Redis等)實(shí)現(xiàn)分布式鎖;
基于Zookeeper實(shí)現(xiàn)分布式鎖;
現(xiàn)在一一分析
基于數(shù)據(jù)庫(kù)的實(shí)現(xiàn)方式
基于數(shù)據(jù)庫(kù)的實(shí)現(xiàn)方式的核心思想是:在數(shù)據(jù)庫(kù)中創(chuàng)建一個(gè)表抡秆,表中包含方法名等字段奕巍,并在方法名字段上創(chuàng)建唯一索引,想要執(zhí)行某個(gè)方法儒士,就使用這個(gè)方法名向表中插入數(shù)據(jù)请琳,成功插入則獲取鎖胡桃,執(zhí)行完成后刪除對(duì)應(yīng)的行數(shù)據(jù)釋放鎖啡专。
DROP TABLE IF EXISTS method_lock
;
CREATE TABLE method_lock
(
id
int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
method_name
varchar(64) NOT NULL COMMENT '鎖定的方法名',
desc
varchar(255) NOT NULL COMMENT '備注信息',
update_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id
),
UNIQUE KEY uidx_method_name
(method_name
) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
(2)想要執(zhí)行某個(gè)方法怜俐,就使用這個(gè)方法名向表中插入數(shù)據(jù):
INSERT INTO method_lock ( method_name
, desc
) VALUES ('methodName', '測(cè)試的methodName');
因?yàn)槲覀儗?duì)method_name做了唯一性約束效床,這里如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫(kù)的話睹酌,數(shù)據(jù)庫(kù)會(huì)保證只有一個(gè)操作可以成功,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖剩檀,可以執(zhí)行方法體內(nèi)容憋沿。
(3)成功插入則獲取鎖,執(zhí)行完成后刪除對(duì)應(yīng)的行數(shù)據(jù)釋放鎖:
delete from method_lock where method_name ='methodName';
注意:這只是使用基于數(shù)據(jù)庫(kù)的一種方法沪猴,使用數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖還有很多其他的玩法辐啄!
使用基于數(shù)據(jù)庫(kù)的這種實(shí)現(xiàn)方式很簡(jiǎn)單,但是對(duì)于分布式鎖應(yīng)該具備的條件來(lái)說(shuō)运嗜,它有一些問(wèn)題需要解決及優(yōu)化:
1壶辜、因?yàn)槭腔跀?shù)據(jù)庫(kù)實(shí)現(xiàn)的,數(shù)據(jù)庫(kù)的可用性和性能將直接影響分布式鎖的可用性及性能担租,所以砸民,數(shù)據(jù)庫(kù)需要雙機(jī)部署、數(shù)據(jù)同步奋救、主備切換岭参;
2、不具備可重入的特性尝艘,因?yàn)橥粋€(gè)線程在釋放鎖之前演侯,行數(shù)據(jù)一直存在,無(wú)法再次成功插入數(shù)據(jù)背亥,所以秒际,需要在表中新增一列,用于記錄當(dāng)前獲取到鎖的機(jī)器和線程信息狡汉,在再次獲取鎖的時(shí)候程癌,先查詢表中機(jī)器和線程信息是否和當(dāng)前機(jī)器和線程相同,若相同則直接獲取鎖轴猎;
3嵌莉、沒(méi)有鎖失效機(jī)制,因?yàn)橛锌赡艹霈F(xiàn)成功插入數(shù)據(jù)后捻脖,服務(wù)器宕機(jī)了锐峭,對(duì)應(yīng)的數(shù)據(jù)沒(méi)有被刪除中鼠,當(dāng)服務(wù)恢復(fù)后一直獲取不到鎖,所以沿癞,需要在表中新增一列援雇,用于記錄失效時(shí)間,并且需要有定時(shí)任務(wù)清除這些失效的數(shù)據(jù)椎扬;
4惫搏、不具備阻塞鎖特性,獲取不到鎖直接返回失敗蚕涤,所以需要優(yōu)化獲取邏輯筐赔,循環(huán)多次去獲取。
5揖铜、在實(shí)施的過(guò)程中會(huì)遇到各種不同的問(wèn)題茴丰,為了解決這些問(wèn)題,實(shí)現(xiàn)方式將會(huì)越來(lái)越復(fù)雜天吓;依賴數(shù)據(jù)庫(kù)需要一定的資源開(kāi)銷贿肩,性能問(wèn)題需要考慮。
基于Redis的實(shí)現(xiàn)方式
1龄寞、選用Redis實(shí)現(xiàn)分布式鎖原因:
(1)Redis有很高的性能汰规;
(2)Redis命令對(duì)此支持較好,實(shí)現(xiàn)起來(lái)比較方便
2物邑、使用命令介紹:
(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è)命令票唆。
3朴读、實(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)行鎖釋放瞒渠。
4良蒸、 分布式鎖的簡(jiǎn)單實(shí)現(xiàn)代碼:
連接redis
redis_client = redis.Redis(host="localhost",
port=6379,
password=password,
db=10)
獲取一個(gè)鎖
lock_name:鎖定名稱
acquire_time: 客戶端等待獲取鎖的時(shí)間
time_out: 鎖的超時(shí)時(shí)間
def acquire_lock(lock_name, acquire_time=10, time_out=10):
"""獲取一個(gè)分布式鎖"""
identifier = str(uuid.uuid4())
end = time.time() + acquire_time
lock = "string:lock:" + lock_name
while time.time() < end:
if redis_client.setnx(lock, identifier):
# 給鎖設(shè)置超時(shí)時(shí)間, 防止進(jìn)程崩潰導(dǎo)致其他進(jìn)程無(wú)法獲取鎖
redis_client.expire(lock, time_out)
return identifier
elif not redis_client.ttl(lock):
redis_client.expire(lock, time_out)
time.sleep(0.001)
return False
釋放一個(gè)鎖
def release_lock(lock_name, identifier):
"""通用的鎖釋放函數(shù)"""
lock = "string:lock:" + lock_name
pip = redis_client.pipeline(True)
while True:
try:
pip.watch(lock)
lock_value = redis_client.get(lock)
if not lock_value:
return True
if lock_value.decode() == identifier:
pip.multi()
pip.delete(lock)
pip.execute()
return True
pip.unwatch()
break
except redis.excetions.WacthcError:
pass
return False
- 測(cè)試剛才實(shí)現(xiàn)的分布式鎖
例子中使用50個(gè)線程模擬秒殺一個(gè)商品,使用–運(yùn)算符來(lái)實(shí)現(xiàn)商品減少伍玖,從結(jié)果有序性就可以看出是否為加鎖狀態(tài)嫩痰。
def seckill():
identifier=acquire_lock('resource')
print(Thread.getName(),"獲得了鎖")
release_lock('resource',identifier)
for i in range(50):
t = Thread(target=seckill)
t.start()
六、基于ZooKeeper的實(shí)現(xiàn)方式
ooKeeper是一個(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方法用于釋放鎖邪蛔。
優(yōu)點(diǎn):具備高可用急黎、可重入、阻塞鎖特性,可解決失效死鎖問(wèn)題叁熔。
缺點(diǎn):因?yàn)樾枰l繁的創(chuàng)建和刪除節(jié)點(diǎn)委乌,性能上不如Redis方式。
上面的三種實(shí)現(xiàn)方式荣回,沒(méi)有在所有場(chǎng)合都是完美的遭贸,所以,應(yīng)根據(jù)不同的應(yīng)用場(chǎng)景選擇最適合的實(shí)現(xiàn)方式心软。
在分布式環(huán)境中壕吹,對(duì)資源進(jìn)行上鎖有時(shí)候是很重要的,比如搶購(gòu)某一資源删铃,這時(shí)候使用分布式鎖就可以很好地控制資源耳贬。
當(dāng)然,在具體使用中猎唁,還需要考慮很多因素咒劲,比如超時(shí)時(shí)間的選取,獲取鎖時(shí)間的選取對(duì)并發(fā)量都有很大的影響诫隅,上述實(shí)現(xiàn)的分布式鎖也只是一種簡(jiǎn)單的實(shí)現(xiàn)腐魂,主要是一種思想