如何在Django模型中管理並發性
為單用戶服務的桌面系統的日子已經過去了 - 網絡應用程序現在正在為數百萬用戶提供服務,許多用戶出現了廣泛的新問題 - 並發問題。
在本文中,我將介紹在Django模型中管理並發性的兩種方法

問題
為了演示常見的並發問題,我們將使用銀行賬戶模型:

開始我們為帳戶實例提供一個簡單的存款和撤銷方法:

這似乎是足夠簡單的,甚至可能通過本地主機的單元測試和集成測試。 但是, 當兩個用戶同時在同一個帳戶上執行操作時會發生什么?
1、用戶A提取帳戶 - 余額為100 $。
2、用戶B提取帳戶 - 余額為100 $。
3、用戶B退出30 $ - 余額更新為100 $ - 30 $ = 70 $。
4、用戶A存款50 $ - 余額更新為100 $ + 50 $ = 150 $。
這里發生了什么?
用戶B要求提取30 $,用戶A存入50 $ - 我們預期余額為120 $,但最終為150 $。
為什么會這樣呢?
在步驟4,當用戶A更新余額時,他在存儲器中存儲的金額已經過時(用戶B已經退出30 $)。
為了防止這種情況發生,我們需要確保我們正在處理的資源在我們正在計算的過程中不會改變。
悲觀的做法
悲觀的做法表明,您應該完全鎖定資源,直到完成它 。 如果沒有人可以在您處理對象時獲取對象上的鎖定,那么可以確保對象沒有被更改。
我們使用數據庫鎖有幾個原因:
1、 數據庫非常擅長管理鎖並保持一致性。
2、數據庫是訪問數據的最低級別 - 獲取最低級別的鎖也會防止其他進程嘗試修改數據。 例如,DB中的直接更新,cron作業,清理任務等。
3、Django應用程序可以在多個進程 (例如工作者)上運行。 在應用程序級別維護鎖將需要大量(不必要的)工作。
要在Django中鎖定一個對象,我們使用select_for_update 。
讓我們用悲觀的方法來實行安全的存款和取款:

按以下步驟:
1、我們在我們的查詢器上使用select_for_update來告訴數據庫鎖定對象,直到事務完成。
2、在數據庫中鎖定一行需要一個數據庫事務 - 我們使用Django的裝飾器transaction.atomic來定義事務。
3、我們使用類方法而不是實例方法 - 我們告訴數據庫要上鎖,然后它會返回鎖的對象給我們。 為了實現這一點,我們需要從數據庫中獲取對象。 如果我們使用self,那么就是在操作一個已經從數據庫中獲取出來的對象,這個對象無法保證自己是沒有被上鎖的。
4、帳戶中的所有操作都在數據庫事務中執行。
讓我們看看如何通過我們的新方法來阻止前面說的情況:
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 不要等待鎖釋放並引發DatabaseError。 為此,我們可以將select_for_update的nowait參數設置為True, …select_for_update(nowait=True) 。
2、選擇相關對象也被鎖定 -當使用select_for_update與select_related時,相關對象也被鎖定。
例如,如果我們選擇與用戶一起select_related帳戶,用戶和帳戶將被鎖定。 如果在存款期間,例如有人正在嘗試更新名字,該更新將失敗,因為用戶對象被鎖定。
如果您正在使用PostgreSQL或Oracle,這可能不是一個問題,由於即將到來的Django 2.0 的新功能 。 在此版本中,select_for_update具有“of”選項,用於顯式地聲明要鎖定查詢中的哪些表 。
我用過去的銀行賬戶示例來展示我們在Django模型中使用的常見模式,歡迎您在本下文中跟進:
https://medium.com/@hakibenita/bullet-proofing-django-models-c080739be4e
樂觀的方法
與悲觀的方法不同,樂觀的方法不需要鎖定對象。 樂觀的方法假定沖突不是很常見 ,並且指出只應確保在更新時對對象沒有做任何更改。
我們如何用Django來實現這樣的事情?
首先,我們添加一列以跟蹤對該對象所做的更改:
然后,當我們更新一個對象時,我們確保版本沒有改變:

接着:
1、我們直接在實例上操作(沒有類方法)。
2、我們依賴於每次更新對象時增加版本的事實。
3、僅當版本沒有更改時,我們才會進行更新:
- 如果對象沒有被修改,我們獲取它,而不是對象被更新 。
- 如果被修改 ,則查詢將返回零記錄,並且對象不會被更新 。
4、Django返回更新行數。 如果“更新”為零,則表示有人在我們獲取對象之后更改了對象。
樂觀鎖定在我們的場景中如何工作:
1. 用戶A獲取帳戶 - 余額為100 $,版本為0。
2. 用戶B提取帳戶 - 余額為100 $,版本為0。
3. 用戶B要求退出30 $:
- 余額更新為100 $ - 30 $ = 70 $。
- 版本增加到1。
4. 用戶A要求存入50 $:
- 計算余額為100 $ + 50 $ = 150 $。
- 該帳戶不存在與版本0 - > 沒有更新。
您需要了解樂觀的方法:
不像悲觀的方法,這種方法需要一個額外的空間和很多規則 。 克服紀律問題的一個方法是抽象這個行為。 django-fsm 使用如上所述的版本字段來實現樂觀鎖定 。 django-optimistic-lock似乎也是這樣做的。 我們沒有使用任何這些包,但靈感來自這里。
在具有大量並發更新的環境中,這種方法可能是浪費的。
這種方法不會對應用程序之外的對象進行修改。 如果您有直接修改數據的其他任務(例如,不通過模型),則需要確保它們也使用該版本。
使用樂觀的方法, 函數可以失敗並返回false。 在這種情況下,我們很可能想要重試操作。 使用悲觀的方法與nowait = False操作不能失敗 - 它將等待釋放鎖。
我應該使用哪一個?
像任何偉大的問題一樣,答案取決於以下:
如果您的對象有很多並發更新,那么悲觀的方式更好。
如果您在ORM之外發生更新(例如,直接在數據庫中),則悲觀的方法更安全。
如果您的方法具有遠程API調用或操作系統調用等副作用,請確保它們是安全的。 還有些事情要考慮 - 遠程通話可能需要很長時間嗎? 遠程電話是否正常(重試安全)?