今天在調試項目開發好的一個模塊的時候,發現了一個很詭異的現象,最后追蹤發現是因為在項目中事務處理有誤所致。這個問題坑了我好一會,所以記錄一下,以免再踩坑。下面開始詳述。
我們都知道 Django 框架提供了很多的開啟事務的方式,這在后面會有詳述。筆者比較喜歡使用的是使用 @transaction.atomic 裝飾的方式來啟動一個事務。因為通過該形式,我們可以在保證了 db 原子操作的同時,還可以自定義事務涉及的模塊范圍。atomic 還可以通過上下文的形式來使用,比如:
with transaction.atomic():
transaction_plalala()
.
.
.
好了。既然如此,那就用起來吧。一陣啪啪啪之后,開發完了,調試的 log 也打了,開測吧。詭異的事情在此發生了。在log中明明打印出來了數據庫中生成的主鍵id,但是在數據庫中死活查不出來。WTF?!通過 SHOW CREATE TABLE xxx 也看到了 xxx 表的自增值已經發現了變化。但咋就沒有了呢?誰動了我的數據?這時候,不得不從 view 開始看起,一直追蹤到了開發的新的模塊。view到調用模塊沒有問題。這是什么情況?有鬼?肯定不是。莫非在某個地方,配置了一個新的事務,而這個事務是包含了整個 view?因為筆者只發現了 view 中有個 raise exception的操作。猜測只能是這樣了。因為我新開發的模塊沒有問題的,這我是在其他的 view 中進行過驗證的。
於是乎,筆者看了下 Django 中的事務開啟的方式,發現了果然有一個將事務綁定到 HTTP 請求上的開啟的方式。它的開啟方式是在 db 中指定 ATOMIC_REQUESTS=True 開啟。打開 settings 文件,找到了對應的配置,果然,問題就是出在這里!咋辦?既然這邊人家已經配置了,在不影響到其他 view(說不定已經依賴了此事務操作)的情況下,怎么去關閉這個配置呢?答案是通過 transaction.non_atomic_requests 裝飾view。好了。測試一下,確實可以了。問題解決了。下面的是官方 Demo:
from django.db import transaction
@transaction.non_atomic_requests
def my_view(request):
do_stuff()
@transaction.non_atomic_requests(using='other')
def my_other_view(request):
do_stuff_on_the_other_database()
在看 Django 中的官方文檔后,發現,其不推薦這么做。原因是如果將事務跟 HTTP 請求綁定到一起的話,view 是依賴於應用程序對數據庫的查詢語句效率和數據庫當前的鎖競爭情況。當流量上來的時候,性能會有影響。那么,該怎么去保證既可以使用事務呢?
第一種就是上面所說的這種,在 database 中通過指定 ATOMIC_REQUESTS 的形式來將事務綁定到HTTP請求上。接觸 view 在事務中的操作的方式是在 view 上裝飾 transaction.non_atomic_requests,前面已經說過,具體也可以閱讀 Django 官方文檔。
default_db = {
"ENGINE": "",
"NAME": "",
"USER": "",
"PASSWORD": "",
"HOST": "",
"PORT": "",
"OPTIONS": "",
"ATOMIC_REQUESTS": True,
}
還有一種方式是筆者喜歡用的那種,通過 transaction.atomic 來更加明確的控制事務。atomic允許我們在執行代碼塊時,在數據庫層面提供原子性保證。 如果代碼塊成功完成, 相應的變化會被提交到數據庫進行commit;如果執行期間遇到異常,則會將該段代碼所涉及的所有更改回滾。
這里,我們在使用此方式的時候還需要注意一點,就是:避免在 atomic里捕獲異常!當一個原子塊執行完退出時,Django會審查是正常提交還是回滾。如果你在原子塊中捕獲了異常的句柄, 你可能就向 Django 隱藏了問題的發生。這可能會導致意想不到的后果。正確捕捉數據庫異常應該是類似上文所講 ,基於atomic 代碼塊來做。若有必要,可以額外增加一層atomic代碼來用於此目的。這種模式還有另一個優勢:它明確了當一個異常發生時,哪些操作將回滾。在底層,Django的事務管理代碼:
- 當進入到最外層的 atomic 代碼塊時會打開一個事務;
- 當進入到內層atomic代碼塊時會創建一個保存點;
- 當退出內部塊時會釋放或回滾保存點;
- 當退出外部塊時提交或回退事物。
你可以通過設置savepoint 參數為 False來使對內層的保存點失效。如果異常發生,若設置了savepoint,Django會在退出第一層代碼塊時執行回滾,否則會在最外層的代碼塊上執行回滾。 原子性始終會在外層事物上得到保證。這個選項僅僅用在設置保存點開銷很明顯時的情況下。它的缺點是打破了上述錯誤處理的原則。
from django.db import transaction
def viewfunc(request):
# This code executes in autocommit mode (Django's default).
do_stuff()
with transaction.atomic():
# This code executes inside a transaction.
do_more_stuff()
Django 支持 autocommit 來為每個SQL語句在執行時都會啟動一個事務。如果想要關閉,可以通過在配置文件中設置 AUTOCOMMIT=False 參數來關閉。這樣,Django 將不能啟用 autocommit,也不能執行任何 commits。這就需要你對每個事物執行明確的commit操作。因此,這最好只用於你自定義的事物控制中間件或者是一些比較奇特的場景。
參考:
