事務作為并發通路資料庫一種有效工具,如果使用不當,也會引起問題。mysql是公司内使用的主流資料庫,預設事務隔離級别是可重複讀。
本文嘗試結合django解釋應用開發中并發通路資料庫可能會遇到的可重複讀引起的問題,希望能幫助大家在開發過程中有效避免類似問題,如果老版本應用中出現這類問題也可以快速定位。
由于django1.3(由于曆史原因,目前藍鲸體系内大多數穩定營運的工具系統用的是django1.3)中該問題最為嚴重,本文先對django1.3環境中的一個應用案例進行分析,說明問題産生的具體原因,然後說明如何有效避免類似問題,最後介紹較新版本django中事務實作原理(django1.6開始已經很好避免本文案例中的大多數情況),并提供一個django1.8中由于對事務使用不當造成的異常案例。
先看下如下這段代碼在django1.3中會有什麼問題:
通過連結http://127.0.0.1:8000/simple_test/請求得到的結果是500錯誤, 如果開啟了debug,則可以看到如下錯誤資訊:
這個執行結果有點讓人吃驚,本應該傳回False才對。
為了快速說明該問題産生的原因,這裡将請求simple_test過程中simple_test和背景任務set_data_in_backend所執行的sql語句分别列印出來:
simple_test響應請求過程執行的sql:
背景任務set_data_in_backend執行過程中執行的sql語句:
結合simple_test響應過程執行的sql語句來看,就比較好了解上面的500錯誤duplicate entry了。響應開始的時候, 開發架構進行了一次使用者登入認證,django設定了autocommit為False,這會直接開啟一個事務,
這時key=6e3247f8-31c5-46d7-a3e9-1c855077ea56的記錄還不存在,由于mysql預設的事務隔離級别是可重複讀,是以在simple_test整個事務期間,都找不到key=6e3247f8-31c5-46d7-a3e9-1c855077ea56的記錄,是以simple_test執行到get_or_create會嘗試插入一條記錄key=6e3247f8-31c5-46d7-a3e9-1c855077ea56,但是在此之前背景任務已經向資料庫中插入了這個key,simple_test執行get_or_create的時候mysql就給直接報一緻性錯誤。
弄明白了這個異常發生的原理之後,我們可能會吓出一身冷汗,如果寫個while循環一直去查詢資料庫中任務的狀态到完成狀态,豈不是死循環了。在django1.3中的确是這樣,因為這個問題django1.3中的cache架構就被送出了Bug,django1.3遵循的是PEP 249Python資料庫API 規範v2.0, 需要将autocommit初試設定為關閉狀态。到了Django1.6之後已經覆寫了這個預設規範并且将autocommit設定為 on. 是以新版本的django出現上述問題的機率會大大降低。
我們可能會有些相對穩定營運的django1.3在生産環境,如果真的出現了類似的問題,可以嘗試從幾個方面修複:
(1)調整中間件,對登入認證完成之後進行一次commit操作。部分因為中間件過早開啟事務的情形有用,比如本文的案例。
(2)發生類似錯誤時,顯式進行一次commit操作。這種解決方式比較直覺,但是如果錯誤本身就發生在事務中則會過早送出事務。
(3)如果隻是需要把記錄拿出來更新,可以考慮直接寫sql更新記錄。
為了說明django1.8中事務實作機制如何與django1.3不一樣,将本文開始時使用案例放在django1.8中執行,調用的sql如下:
從django1.8中執行的sql可以看出,Django1.8的預設行為是運作在自動送出模式下。任何一個查詢都立即被送出到資料庫中,除非顯示激活一個事務。最後,django1.8隻是将這種可重複讀引起問題的機率降低了很多,如果我們在事務中處理不當,也會引起類似問題,django本文最開始的例子進行稍微調整,在django1.8中運作一樣會報錯。