select_for_update
為了演示常見的并發(fā)問題担敌,我們將使用銀行賬戶模型,開始我們?yōu)閹魧嵗峁┮粋€簡單的存款和撤銷方法:
當兩個用戶同時在同一個帳戶上執(zhí)行操作時會發(fā)生什么?
1、用戶A提取帳戶 - 余額為100$。
2嚼黔、用戶B提取帳戶 - 余額為100$。
3惜辑、用戶B退出30$ - 余額更新為100$ - 30$ = 70$唬涧。
4、用戶A存款50$ - 余額更新為100$ + 50$ = 150$盛撑。
這里發(fā)生了什么碎节?用戶B要求提取30$,用戶A存入50$ - 我們預期余額為120$抵卫,但最終為150$狮荔。
為什么會這樣呢?
在步驟4介粘,當用戶A更新余額時殖氏,他在存儲器中存儲的金額已經(jīng)過時(用戶B已經(jīng)退出30$)。
為了防止這種情況發(fā)生姻采,我們需要確保我們正在處理的資源在我們正在計算的過程中不會改變雅采。
悲觀的做法表明,您應該完全鎖定資源,直到完成它 总滩。 如果沒有人可以在您處理對象時獲取對象上的鎖定纲堵,那么可以確保對象沒有被更改。
我們使用數(shù)據(jù)庫鎖有幾個原因:
1闰渔、 數(shù)據(jù)庫非常擅長管理鎖并保持一致性席函。
2、數(shù)據(jù)庫是訪問數(shù)據(jù)的最低級別 - 獲取最低級別的鎖也會防止其他進程嘗試修改數(shù)據(jù)冈涧。 例如茂附,DB中的直接更新,cron作業(yè)督弓,清理任務(wù)等营曼。
3、Django應用程序可以在多個進程 (例如工作者)上運行愚隧。 在應用程序級別維護鎖將需要大量(不必要的)工作蒂阱。
要在Django中鎖定一個對象,我們使用select_for_update 狂塘。
1录煤、我們在我們的查詢器上使用select_for_update來告訴數(shù)據(jù)庫鎖定對象,直到事務(wù)完成荞胡。
2妈踊、在數(shù)據(jù)庫中鎖定一行需要一個數(shù)據(jù)庫事務(wù) - 我們使用Django的裝飾器transaction.atomic來定義事務(wù)。
3泪漂、我們使用類方法而不是實例方法 - 我們告訴數(shù)據(jù)庫要上鎖廊营,然后它會返回鎖的對象給我們。 為了實現(xiàn)這一點萝勤,我們需要從數(shù)據(jù)庫中獲取對象露筒。 如果我們使用self,那么就是在操作一個已經(jīng)從數(shù)據(jù)庫中獲取出來的對象纵刘,這個對象無法保證自己是沒有被上鎖的邀窃。
4、帳戶中的所有操作都在數(shù)據(jù)庫事務(wù)中執(zhí)行假哎。
讓我們看看如何通過我們的新方法來阻止前面說的情況:
1瞬捕、用戶A要求退出30$:
用戶A獲取帳戶上的鎖。
余額為100美元舵抹。
2肪虎、用戶B要求存入50$:
嘗試獲取鎖定帳戶失敗(由用戶A鎖定)惧蛹。
用戶B等待鎖釋放 扇救。
3刑枝、用戶A撤回30$:
余額是70$。
帳戶上的用戶A的鎖定被釋放 迅腔。
4装畅、用戶B獲取帳戶上的鎖。
余額是70$沧烈。
新余額為70 + 50 = 120$掠兄。
5、賬號上用戶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)對象也被鎖定 -當使用select_for_update與select_related時,相關(guān)對象也被鎖定锨并。
例如,如果我們選擇與用戶一起select_related帳戶睬棚,用戶和帳戶將被鎖定第煮。 如果在存款期間,例如有人正在嘗試更新名字抑党,該更新將失敗包警,因為用戶對象被鎖定。
如果您正在使用PostgreSQL或Oracle底靠,這可能不是一個問題害晦,由于即將到來的Django 2.0 的新功能 。 在此版本中暑中,select_for_update具有“of”選項壹瘟,用于顯式地聲明要鎖定查詢中的哪些表 。