繼上一篇《Mysql事務探索及其在Django中的實踐(一)》交代完問題的背景和Mysql事務基礎后,這一篇主要想介紹一下事務在Django中的使用以及實際應用給我們帶來的效率提升。
首先貼上Django官方文檔中關於Database Transaction一章的介紹:https://docs.djangoproject.com/en/1.9/topics/db/transactions/。
在Django中實現事務主要有兩種方式:第一種是基於django ORM框架的事務處理,第二種是基於原生地執行SQL語句的transaction處理。
基於django ORM框架的事務處理
默認情況下,django的事務處理是自動提交(auto-commit),即在執行model.save(),model.delete()時,所有的改動會被立即提交,相當於數據庫設置了auto commit,沒有任何隱藏的rollback。
在網上查了一些資料,了解到django手動配置事務的方式主要有三種:第一種是將一個http request的所有數據庫操作包裹在一個transaction中,第二種是通過transaction中間件對http請求的事務攔截,第三種是自己在view中通過裝飾器靈活控制事務(我們的平台最后用的就是這一種)。
1.將一個http request的所有數據庫操作包裹在一個transaction中
這種方式配置非常簡單,只需要在settings.py中的database配置中加入‘ATOMIC_REQUESTS’: True即可。如圖1所示:

圖1 Database中加入'ATOMIC_REQUESTS':True
通過這種配置,django在調每個view方法之前會開始一個事務,當且僅當該響應沒有任何問題,django才會提交這個事務;如果view中出現了異常,則django會回滾該事務。這樣做的好處顯而易見,就是安全簡便,但是隨着網站的流量變大,如果每個view被調用時都打開一個事務就會變得有點繁重,從而會降低網站的效率。它對性能的影響取決於你的應用的查詢效率以及你的數據庫處理鎖的能力。此外,使用這種方式,回退的只是數據庫的狀態,而不包括其他非數據庫項的操作,例如發送email等。
2.通過transaction中間件對http請求的事務攔截
配置方法是在settings.py中配置MIDDLEWARE_CLASSES,如圖2所示:

圖2 transaction中間件配置
需要注意的是,這樣配置之后,與你中間件的配置順序是有很大關系的。在 TransactionMiddleware 之后的所有中間件都會受到事務的控制。但CacheMiddleware,UpdateCacheMiddleware,FetchFromCacheMiddleware 這些中間件不會受到影響,因為cache機制有自己的處理方式,用了內部的connection來處理。另外TransactionMiddleware 只對默認的數據庫配置有效,如果要對另外的數據連接用這種方式,必須自己實現中間件。(此處必須聲明,對於這種方法,本人沒有仔細研究過,只是借鑒了一下網上的資料)
3.自己在view中通過裝飾器靈活控制事務
最后種方式,通過裝飾器靈活配置,也是我們平台最后采用的方式。
1)@transaction.autocommit
django默認的事務處理,采用此裝飾模式這種方式可以忽視全局的transaction配置。
2)@transaction.commit_on_success

采用此裝飾模式,當此方法的所有工作完成后,才會提交事務。
3)@transaction.commit_manually

采用這種方式,你可以自由地隨意提交或回滾事務,完全自己處理。如果沒有調用commit()或rollback(),則程序會拋出TransactionManagementError異常。
基於原生地執行SQL語句的transaction處理
再來講講Django中第二種事務處理方式,即用原生地執行SQL語句的方式。

圖3 原生地執行SQL語句中的事務處理
這種處理方式比較簡單,以圖3中的方法為例,首先定義了一個游標cursor,通過cursor任意地執行sql語句,最后通過transaction.commit_unless_managed()來提交事務。
延伸
上面介紹了Django中的兩種事務處理的方式,到這里我突然想到一個問題:
如果一個方法中既包含了裝飾器@transaction.commit_on_success,又執行了原生SQL語句的事務提交,當方法出現異常導致事務回滾時,原生的SQL語句所提交的事務會不會被回滾?
為了驗證這個問題,我用Flask寫了兩個接口來進行驗證:

接口delete_and_insert和接口delete_and_insert2都是先通過cursor事務提交執行清除表,然后往表里循環插入數據,當滿足條件i=480的時候,拋出一個ValueError。唯一的區別就是接口1中采用了裝飾器@transaction.commit_on_success,而接口2中沒有。實際執行發現:
1.在調接口1時,原數據庫表中的數據不會變化,說明通過cursor執行清除表的操作也會回滾。
2.在調接口2時,原數據庫表中的數據被刪除,數據庫留下的是新的數據:所有的name都為user,而age從1到480。
從而證明,當view方法加了裝飾器@transaction.commit_on_success后,即使view中使用了cursor執行原生sql語句,並執行了transaction.commit_unless_managed(),但是如果view中有異常拋出,整個view方法的內容都會回滾。
實際應用
最后,回歸最初的問題本身,當我把事務應用到我們平台的后台接口中后,發現了一個意外的驚喜:接口A的執行時間從原來的5-10分鍾一下子縮短到了幾秒鍾就完成了。欣喜之余仔細思考了一下才覺得性能顯著提升的原因應該是:在應用事務之前,所有SQL語句都是自動提交的,每插入一條數據,數據庫表的索引可能就需要重建一次,當大量的sql語句逐一插入時,數據庫表的索引就需要不斷地重建,其中就需要耗費大量的時間。而在應用事務之后,所有的插入是一次性提交,數據庫表的索引只需要重建一次,大大減少了開銷。
這也驗證了數據庫的索引不是萬能的,合理的建立索引確實能大大地優化查詢速度,因為索引的存儲結構就像一本字典一樣,我們在查找某個特定的字時會根據拼音的首字母的方式先找到該字的第一個字母所在頁數,然后直接跳到那一頁往后去翻。然而這也決定了字典在初始存儲這些字時就需要根據這些字的特點將每個字放在特定的存儲位置。當有新的字加入時,為了插入到特定的位置,就必須重新建立映射關系。
補充:合理建立索引
下面是我工作中搜集的一些關於索引建立的規則,也歡迎大家參考,指正:
