對于資源加鎖的一些思考
本文主要介紹"微信搶票"與“THU琴房預約”項目中處理資源訪問競爭問題的一些對比與思考。
1. 項目簡介
“微信搶票“是一個基于Django框架捎琐、用于微信公眾號的搶票系統(tǒng)会涎,主要功能是創(chuàng)建活動、處理搶票瑞凑。
"THU琴房預約"是一個基于Koa框架末秃、用于微信小程序的琴房資源預約系統(tǒng),主要功能是管理琴房籽御,處理預約练慕。
這兩個項目均比較復雜,這里僅僅挑出存在資源訪問競爭的模塊進行分析技掏。
2. 加鎖問題
2.1 為什么要加鎖剪个?
對于以上兩個項目箕速,我們首先考慮其核心流程:
- 用戶看到這個活動仍然有余票之后避归,用戶開始搶票(“微信搶票“)
- 用戶看到這個琴房這個時間段沒有被占用昵慌,用戶開始預約("THU琴房預約")
考慮如下情形:
用戶A和B同時看到這個活動還剩一張票秃臣,(即涧衙,這兩個用戶均認為自己用獲得此資源的能力),A和B同時搶票奥此,(因為這兩個用戶的系統(tǒng)通過先前的數(shù)據(jù)獲取已經(jīng)判定這兩個人均可以搶票)弧哎。于是,A和B同時獲得了這張票稚虎,同時總票數(shù)變成了-1撤嫩。即出現(xiàn)邏輯錯誤。
因此為了使得一個資源不能被兩個人同時使用蠢终,我們提出了”加鎖“的方案序攘。
2.2 什么情況加鎖?
經(jīng)過上面的分析寻拂,我們可以發(fā)現(xiàn)問題:在A程奠、B之中某個人搶到票之后,另一個人依然依據(jù)的是之前所獲得的數(shù)據(jù)祭钉,在老數(shù)據(jù)中顯示仍然有余票瞄沙。
2.2.1 有讀有寫才加鎖
上述過程其實是有鎖的,在A和B更改數(shù)據(jù)的過程中其實有一個”寫鎖“,使得一個資源只能被一個進程更改距境。但是尷尬的是申尼,A和B一前一后對數(shù)據(jù)進行的更改,一個把余票從1改為0垫桂,另一個從0再改為-1师幕。
如果讓A、B在他們更改數(shù)據(jù)的時候也準確獲得到能否更改此數(shù)據(jù)就可以了伪货。這樣后面更改的人會獲取到“此時資源已經(jīng)被占用了们衙,不能搶占了“的信息,即一張票不會重復被搶占碱呼。
所以解決方案就是蒙挑,我把”讀取”與“更改”打包成一個事件M,我們使得對于M愚臀,每次只能有一個資源進行M操作即可忆蚀,這樣保證了,A和B按照順序執(zhí)行M姑裂,假設(shè)A首先執(zhí)行完成M操作馋袜,B再執(zhí)行的時候,會首先獲取”是否可以搶票“舶斧,這樣完成了資源的加鎖欣鳖。
2.2.2 新增刪除不必加鎖
這里是指”新增與更改“、”刪除與更改“不必加鎖呢茴厉,因為這兩個操作不會造成數(shù)據(jù)錯誤(就是一張票被兩個人搶到)泽台。
新增:在新增之前,還沒有這條數(shù)據(jù)矾缓,所以不必擔心與更改的競爭
-
刪除:在刪除之后怀酷,不會有這條數(shù)據(jù),所以也不必擔心與更改的競爭
但是刪除需要的是:在刪除一個活動之前需要查看這個活動是否有票在用戶手中嗜闻,并且票可用蜕依。
2.3 如何加鎖(樂觀與悲觀、阻塞與非阻塞)琉雳?
-
悲觀與樂觀:
剛剛提到的使得A和B只能有一個人在執(zhí)行操作M的方案样眠,其實是”悲觀鎖“,就是指翠肘,不允許無依據(jù)的更改檐束。同時還有樂觀鎖,就是將檢查放在更改后锯茄,如果檢查數(shù)據(jù)不符合規(guī)范厢塘,則再改回去茶没。
-
阻塞與非阻塞:
阻塞鎖是指在A進行操作的時候,B會等待晚碾,在A訪問結(jié)束之后抓半,B獲取資源,會形成一個阻塞隊列格嘁。非阻塞鎖是指笛求,A進行操作的時候,B不會等待糕簿,直接返回探入。
兩者各有利弊。比如票數(shù)這個數(shù)字懂诗,這就是需要阻塞鎖的蜂嗽,因為在它被改了一次之后,依然可以更改殃恒;但是植旧,比如對于每一張票,如果要刪除一張票离唐,兩次刪除操作不需要等待病附,因為改過一次之后就不用再更改了。
2.4 真的鎖住了嗎亥鬓?
2.4.1 定義事務的缺陷
在Django中采用了我們提到的打包操作的方案完沪,定義了事務with transaction,即同一時刻嵌戈,只能有一個進程在這個”事務“所定義的代碼流程中覆积。對于搶票這一個操作,這樣是可以保證票數(shù)資源被鎖住的咕别。因為票數(shù)只可能被搶票更改技健。
但是如果增加一個操作写穴,比如管理員可以隨之新增或者減小若干票惰拱。
這樣對于票數(shù)這個資源,”搶票“和”管理員更改“這兩個操作均可以更改啊送,這就十分尷尬了偿短,因為,我們?nèi)绻虬麺(查看信息+搶票)馋没,N(查看信息+管理員更改)昔逗,這樣不能解決M和N同時發(fā)生的情況。
其實可以這樣打包一個MN(判斷操作+選擇M或者N)篷朵,這樣所有操作均調(diào)用一個函數(shù)勾怒,這個函數(shù)會變得非常龐大婆排。
這不是一個好方法。
2.4.2 使用信號量
對于前面的情形笔链,其實我們只需要讓M和N也不會在同一時刻發(fā)生即可段只。
這樣就可以定義一個信號量X,如果X為1鉴扫,可以發(fā)生M和N之中的一個赞枕,如果X為0,則不能發(fā)生坪创。在M和N發(fā)生前炕婶,首先獲取這個信號量,如果獲取到莱预,就把信號量置0柠掂,在結(jié)束值置1。
對于這個方案依沮,在RedLock中有實現(xiàn)陪踩。之后會有一個詳細的介紹。