我前段時間在寫代碼的時候,常常考慮併發問題,對事物的安全性、隔離級別須要更深的瞭解,因此翻看了網上絕大部分關於事務的文章。可是看了以後仍是有些疑惑,例如事務的四種隔離級別,雖然有些文章舉出了生動的例子,但並無提到編程中的如何選擇使用。java
大部分介紹事務的文章,都是介紹什麼事務隔離級別的、各類鎖的概念,好像舉得概念越多,就顯示做者了知識更豐富同樣,然而並無實際編程的例子,就像英文教科書般將本該實際運用的東西變成一種學術,就算看懂講是什麼東西也沒辦法使用。這種教科書式、百科式的文章貽害不淺,所以我才寫這篇文章。sql
事務簡單來講就是要麼一塊兒過,要麼所有取消,保證數據的完整性,在普通狀況下,事務就是這麼簡單,提交回滾罷了。然而遇到併發問題,數據安全問題,這點了解是不夠的。
數據庫
事務有4種隔離級別,爲何是4種而沒有5種、6種?多是研究數據庫的鼻祖們總結最後得出來的吧。那麼下面我要先引用網上絕大部分關於這4種級別的介紹,如下爲網上摘抄。編程
在介紹4種事務隔離級別前,須要三個概念:安全
1. 髒讀:一個事務讀取到另外一個事務還沒有提交的數據。併發
2. 不可重複讀:同一事務,兩次讀取同一數據,獲得不一樣的結果。測試
3. 幻讀:同一事務,用相同的條件讀 取兩次,獲得的結果集數據條數不一樣(數據條數多了或者少了)。spa
而後爲了解決這些個問題,數據庫有了4種隔離級別:線程
1. TRANSACTION_READ_UNCOMMITTED:防止更新丟失,容許髒讀、不可重複讀、幻讀。事務
2. TRANSACTION_READ_COMMITTED:防止髒讀,容許不可重複讀、幻讀,這也是多數數據庫默認的隔離級別。
3. TRANSACTION_REPEATABLE_READ:防止髒讀、不可重複讀,容許幻讀。
4. TRANSACTION_SERIALIZABLE:防止髒讀、不可重複讀,幻讀。(事務徹底串行化執行,事務一個一個按順序依次執行,能夠不會產生併發問題。)
還附帶了一張表:
丟失更新 | 髒讀 |
不可重複讀 | 幻讀 | |
未提交讀 |
N | Y | Y | Y |
已提交讀 |
N | N | Y | Y |
可重複讀 |
N | N | N | Y |
串行化 |
N | N | N | N |
還有一些文章對隔離級別的理解:「級別越高越能保證數據完整性一致性」,「一個級別解決一個問題」,有的還畫出隔離級別與併發的關係座標圖。。。
網上幾乎全部文章都是這麼寫,其實有些數據庫官方的文檔也是這麼解釋的。這麼解釋從某個角度是正確的,可是我就有些疑問了,「READ_UNCOMMITTED」這個隔離級別不就是廢物了麼?爲何Oracle和SQLservice的默認隔離級別是READ_COMMITTED,Mysql的默認隔離級別是REPEATABLE_READ,Oracle這種商業的數據庫默認隔離級別安全性比開源的數據庫還要低?java項目使用的Hibernate爲的是跨數據庫,能從Oracle和Mysql之間切換,那麼切換後默認事務隔離級別變了不會影響安全性麼?解決幻讀必定要使用SERIALIZABLE麼,怎麼我看不少項目基本上都沒用這個呢?等等各類疑問。
經過結合實際案例研究,我認爲事務隔離級別應該這麼劃分:
REPEATABLE_READ < READ_COMMITTED < READ_UNCOMMITTED < SERIALIZABLE
並且,我要說的是我這種隔離級別的劃分纔是合理的!可能你從沒見過有人這麼定義隔離級別,並且官方都不這麼定義的!我只用事實說話,下面我結合實際編程的例子,分析爲何事務隔離級別爲何按我那樣劃分才合理。
舉個簡單的例子:註冊。
咱們在作註冊這個功能的時候,一般要進行這麼個步驟:
在這個需求下,是不能簡單說使用哪一種事務隔離級別好的,這是分開幾個步驟,並非一瞬間,有可能在這幾個步驟之間,其餘事務操做了數據庫,因此必須結合時間順序!
場景1:
假如使用REPEATABLE_READ ,例如Mysql,它在實現該隔離級別是用MVCC,即讀取記錄的時候過濾時間戳,即便在當前線程的過程當中其餘事務對數據庫進行增長、修改或刪除,也是看不到的,達到了數據的一致性。然而這個」一致性「在這個場景並無好處,由於若線程1和線程2都註冊同一個用戶名,線程2在時間1的時候提交了數據線程1看不到,會認爲該用戶名還沒註冊,容許用戶註冊,當插入數據庫的時候,因爲unique約束報錯了。
在這種狀況下,使用READ_COMMITTED ,能看到其餘事務提交了的數據,明顯更具備準確的檢驗結果!
場景2:
線程2的提交時間變成了時間3,此時READ_COMMITTED 就沒什麼做用了,這種場景下下READ_UNCOMMITTED 就發揮做用了。READ_UNCOMMITTED 實際上是一個更增強大的事務隔離級別,連其餘事務未提交的東西都能看到,尤爲在一些簡單的邏輯上,插入數據庫緊接着下一步就是提交無誤,這種狀況下是比較適合使用READ_UNCOMMITTED 這個隔離級別的。
通過我在Mysql測試當隔離級別爲READ_UNCOMMITTED 的時候,同時具備READ_COMMITTED 和READ_UNCOMMITTED 的能力,即能看到其餘線程提交了的數據和未提交的數據!
場景3:
此場景中兩個事物在檢查用戶名是否存在的時候,數據庫確實沒有記錄,連未提交的也沒有,它們在插入數據庫的時候僅僅相差了一步!在Mysql中的測試狀況下是,線程1和2都會進行插入記錄這個動做,可是因爲有惟一約束,線程1較晚插入會受到阻塞而不是報錯,由於它要看另一個線程是打算提交仍是回滾,若是回滾,線程1將繼續執行,若是線程2提交了,線程1報錯。
這種狀況除了使用SERIALIZABLE隔離級別,其餘隔離級別都不能防止另外一個線程報錯,然而SERIALIZABLE隔離級別是徹底阻塞,如該場景線程2先開啓事務,線程1連檢查用戶名是否存在這一步都會阻塞,但其餘查詢業務也同樣受到阻塞,從效率來講來是很是很差的。
舉這個例子,是想借此介紹下各個事務隔離級別在同一個場景下的表現,而不是探討怎麼解決該例子的問題。我舉的這個例子,「用戶名」有惟一約束,這已是保證數據正確性的終極防線了,檢查用戶名是否存在的做用,只是在頁面註冊的時候用Ajax提早檢查,增長用戶體驗而已,即便沒有數據的安全性也是能達到的,因此我以爲這個功能使用REPEATABLE_READ、READ_COMMITTED或READ_UNCOMMITTED都是能夠的。
瞭解了事務隔離級別的真正意義後,咱們要探討下併發問題,由於事物隔離級別也是由於併發而產生的。上面我舉了一個註冊的例子,具備併發問題,可是問題並不嚴重,由於「用戶名」這個字段有unique約束,即便不作插入前的校驗步驟,也不會致使出現兩個相同的用戶名這種錯誤。
那麼如今來討論一個沒有約束的例子,沒有約束的話,就必須靠解決併發問題來保證數據安全性。
例子:買票
咱們在作這個功能的時候,須要進行這麼個步驟:
因爲數據庫沒有正整數類型,沒有約束來防止數據出錯,若是出現註冊那個例子中的場景3,是會致使餘票變爲負數。
那麼,其餘三個事物隔離級別都沒有100%的做用,是否是得出殺手鐗SERIALIZABLE了?答案是否的,由於使用SERIALIZABLE隔離級別會徹底阻塞其餘事物,會致使當有一我的進行購票的時候,其餘對該事務涉及的表的查詢都要等待,這種狀況怎麼能忍?除非只有該事務對涉及的表有操做,但這種狀況幾乎是不可能的,即便有也不能保證未來業務擴充。
解決辦法1:利用synchronized讓方法同步執行,是解決這個併發場景的常見辦法。
解決辦法2:Hibernate樂觀鎖,也就是@Version註解。在存放着「餘票」字段的表中,增長一個int型字段,同時使用@Version註解,這個字段就表示版本號,每次修改版本號就會遞增。當前線程持久化時若是檢測到版本號變化,即有其餘線程修改過該記錄,將會拋異常,咱們能夠在拋異常的時候作一些其餘措施,或者什麼都不作。
我對事物隔離級別的見解,還有解決併發問題的思路可能比較淺薄,但仍是但願這篇文章能幫到你們,同時歡迎留言探討共同進步!