select_for_update
為了演示常見的并發(fā)問題,我們將使用銀行賬戶模型,開始我們?yōu)閹魧?shí)例提供一個(gè)簡(jiǎn)單的存款和撤銷方法:
當(dāng)兩個(gè)用戶同時(shí)在同一個(gè)帳戶上執(zhí)行操作時(shí)會(huì)發(fā)生什么?
1、用戶A提取帳戶 - 余額為100$。
2、用戶B提取帳戶 - 余額為100$。
3、用戶B退出30$ - 余額更新為100$ - 30$ = 70$。
4、用戶A存款50$ - 余額更新為100$ + 50$ = 150$。
這里發(fā)生了什么?用戶B要求提取30$,用戶A存入50$ - 我們預(yù)期余額為120$,但最終為150$。
為什么會(huì)這樣呢?
在步驟4,當(dāng)用戶A更新余額時(shí),他在存儲(chǔ)器中存儲(chǔ)的金額已經(jīng)過時(shí)(用戶B已經(jīng)退出30$)。
為了防止這種情況發(fā)生,我們需要確保我們正在處理的資源在我們正在計(jì)算的過程中不會(huì)改變。
悲觀的做法表明,您應(yīng)該完全鎖定資源,直到完成它 。 如果沒有人可以在您處理對(duì)象時(shí)獲取對(duì)象上的鎖定,那么可以確保對(duì)象沒有被更改。
我們使用數(shù)據(jù)庫鎖有幾個(gè)原因:
1、 數(shù)據(jù)庫非常擅長(zhǎng)管理鎖并保持一致性。
2、數(shù)據(jù)庫是訪問數(shù)據(jù)的最低級(jí)別 - 獲取最低級(jí)別的鎖也會(huì)防止其他進(jìn)程嘗試修改數(shù)據(jù)。 例如,DB中的直接更新,cron作業(yè),清理任務(wù)等。
3、Django應(yīng)用程序可以在多個(gè)進(jìn)程 (例如工作者)上運(yùn)行。 在應(yīng)用程序級(jí)別維護(hù)鎖將需要大量(不必要的)工作。
要在Django中鎖定一個(gè)對(duì)象,我們使用select_for_update 。
1、我們?cè)谖覀兊牟樵兤魃鲜褂胹elect_for_update來告訴數(shù)據(jù)庫鎖定對(duì)象,直到事務(wù)完成。
2、在數(shù)據(jù)庫中鎖定一行需要一個(gè)數(shù)據(jù)庫事務(wù) - 我們使用Django的裝飾器transaction.atomic來定義事務(wù)。
3、我們使用類方法而不是實(shí)例方法 - 我們告訴數(shù)據(jù)庫要上鎖,然后它會(huì)返回鎖的對(duì)象給我們。 為了實(shí)現(xiàn)這一點(diǎn),我們需要從數(shù)據(jù)庫中獲取對(duì)象。 如果我們使用self,那么就是在操作一個(gè)已經(jīng)從數(shù)據(jù)庫中獲取出來的對(duì)象,這個(gè)對(duì)象無法保證自己是沒有被上鎖的。
4、帳戶中的所有操作都在數(shù)據(jù)庫事務(wù)中執(zhí)行。
讓我們看看如何通過我們的新方法來阻止前面說的情況:
1、用戶A要求退出30$:
用戶A獲取帳戶上的鎖。
余額為100美元。
2、用戶B要求存入50$:
嘗試獲取鎖定帳戶失?。ㄓ捎脩鬉鎖定)。
用戶B等待鎖釋放 。
3、用戶A撤回30$:
余額是70$。
帳戶上的用戶A的鎖定被釋放 。
4、用戶B獲取帳戶上的鎖。
余額是70$。
新余額為70 + 50 = 120$。
5、賬號(hào)上用戶B的鎖定被釋放,余額為120$。Bug消失了!
這里你需要了解select_for_update
1、在我們的方案中,用戶B等待用戶A釋放鎖,我們可以告訴Django 不要等待鎖釋放并引發(fā)DatabaseError。 為此,我們可以將select_for_update的nowait參數(shù)設(shè)置為True, …select_for_update(nowait=True) 。
2、選擇相關(guān)對(duì)象也被鎖定 -當(dāng)使用select_for_update與select_related時(shí),相關(guān)對(duì)象也被鎖定。
例如,如果我們選擇與用戶一起select_related帳戶,用戶和帳戶將被鎖定。 如果在存款期間,例如有人正在嘗試更新名字,該更新將失敗,因?yàn)橛脩魧?duì)象被鎖定。
如果您正在使用PostgreSQL或Oracle,這可能不是一個(gè)問題,由于即將到來的Django 2.0 的新功能 。 在此版本中,select_for_update具有“of”選項(xiàng),用于顯式地聲明要鎖定查詢中的哪些表 。