鎖、事務、隔離級別--readonly

爲什麼需要鎖?

因爲數據庫要解決併發控制問題。在同一時刻,可能會有多個客戶端對Table1.rown進行操作,比如有的在讀取該行數據,其他的嘗試去刪除它。爲了保證數據的一致性,數據庫就要對這種併發操作進行控制,因此就有了鎖的概念。

鎖的分類

從對數據操作的類型(讀\寫)分

讀鎖(共享鎖):針對同一塊數據,多個讀操作可以同時進行而不會互相影響。

寫鎖(排他鎖):噹噹前寫操作沒有完成前,它會阻斷其他寫鎖和讀鎖。

從鎖定的數據範圍分

表鎖

行鎖

爲了儘可能提高數據庫的併發度,每次鎖定的數據範圍越小越好,理論上每次只鎖定當前操作的數據的方案會得到最大的併發度,但是管理鎖是很耗資源的事情(涉及獲取,檢查,釋放鎖等動作),因此數據庫系統需要在高併發響應和系統性能兩方面進行平衡,這樣就產生了“鎖粒度(Lock granularity)”的概念

鎖粒度(Lock granularity)

表鎖:管理鎖的開銷最小,同時允許的併發量也最小的鎖機制。MyIsam存儲引擎使用的鎖機制。當要寫入數據時,把整個表都鎖上,此時其他讀、寫動作一律等待。在MySQL中,除了MyIsam存儲引擎使用這種鎖策略外,MySql本身也使用表鎖來執行某些特定動作,比如alter table.

行鎖:可以支持最大併發的鎖策略。InnoDB和Falcon兩張存儲引擎都採用這種策略。

MySql是一種開放的架構,你可以實現自己的存儲引擎,並實現自己的鎖粒度策略,不像Oracle,你沒有機會改變鎖策略,Oracle採用的是行鎖。

事務(Transaction)

從業務角度出發,對數據庫的一組操作要求保持4個特徵:

Atomicity:原子性

Consistency:一致性,

Isolation:隔離性

Durability:持久性

爲了更好地理解ACID,以銀行賬戶轉賬爲例:

1 START TRANSACTION;

2 SELECT balance FROM checking WHERE customer_id = 10233276;
3 UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
4 UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
5 COMMIT;

原子性:要麼完全提交(10233276的checking餘額減少200,savings 的餘額增加200),要麼完全回滾(兩個表的餘額都不發生變化)

一致性:這個例子的一致性體現在 200元不會因爲數據庫系統運行到第3行之後,第4行之前時崩潰而不翼而飛,因爲事物還沒有提交。

隔離性:允許在一個事務中的操作語句會與其他事務的語句隔離開,比如事務A運行到第3行之後,第4行之前,此時事務B去查詢checking餘額時,它仍然能夠看到在事務A中被減去的200元,因爲事務A和B是彼此隔離的。在事務A提交之前,事務B觀察不到數據的改變。

持久性:這個很好理解。

事務跟鎖一樣都會需要大量工作,因此你可以根據你自己的需要來決定是否需要事務支持,從而選擇不同的存儲引擎。

隔離級別(Isolation Level)

 SQL標準定義了4類隔離級別,包括了一些具體規則,用來限定事務內外的哪些改變是可見的,哪些是不可見的。低級別的隔離級一般支持更高的併發處理,並擁有更低的系統開銷。
Read Uncommitted(讀取未提交內容)

       在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。本隔離級別很少用於實際應用,因爲它的性能也不比其他級別好多少。讀取未提交的數據,也被稱之爲髒讀(Dirty Read)。
Read Committed(讀取提交內容)

       這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變。這種隔離級別 也支持所謂的不可重複讀(Nonrepeatable Read),因爲同一事務的其他實例在該實例處理其間可能會有新的commit,所以同一select可能返回不同結果。
Repeatable Read(可重讀)

       這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在併發讀取數據時,會看到同樣的數據行。不過理論上,這會導致另一個棘手的問題:幻讀 (Phantom Read)。簡單的說,幻讀指當用戶讀取某一範圍的數據行時,另一個事務又在該範圍內插入了新行,當用戶再讀取該範圍的數據行時,會發現有新的“幻影” 行。InnoDB和Falcon存儲引擎通過多版本併發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題。

Serializable(可串行化) 
這是最高的隔離級別,它通過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。

         這四種隔離級別採取不同的鎖類型來實現,若讀取的是同一個數據的話,就容易發生問題。例如:

         髒讀(Drity Read):某個事務已更新一份數據,另一個事務在此時讀取了同一份數據,由於某些原因,前一個RollBack了操作,則後一個事務所讀取的數據就會是不正確的。—–針對未提交事物或回滾事物。

         不可重複讀(Non-repeatable read):在一個事務的兩次查詢之中數據不一致,這可能是兩次查詢過程中間插入了一個事務更新的原有的數據。—–針對已提交的更新或刪除操作。

         幻讀(Phantom Read):在一個事務的兩次查詢中數據筆數不一致,例如有一個事務查詢了幾列(Row)數據,而另一個事務卻在此時插入了新的幾列數據,先前的事務在接下來的查詢中,就會發現有幾列數據是它先前所沒有的。—–針對已提交的插入操作。



readonly詳解


1. 首先最根本的,我們要看一下數據庫中對於readonly事務的定義!

 

第一個帖子中已經給出了結論:

引用代碼  收藏代碼
  1. Oracle默認情況下保證了SQL語句級別的讀一致性,即在該條SQL語句執行期間,它只會看到執行前點的數據狀態,而不會看到執行期間數據被其他SQL改變的狀態。   
  2.   
  3. 而Oracle的只讀查詢(read-only transaction)則保證了事務級別的讀一致性,即在該事務範圍內執行的多條SQL都只會看到執行前點的數據狀態,  
  4. 而不會看到事務期間的任何被其他SQL改變的狀態。   

 

 這個是Oracle對於只讀事務的描述,要保證事務級別的讀一致性有兩種方式,第一個帖子中也給出了結論:

Java代碼  收藏代碼
  1. 一是用SET TRANSACTION ISOLATION LEVEL SERIALIZABLE  
  2. 二是用SET TRANSCATION READ ONLY  

 

我們來做一下測試(採用第二種方式):

編寫一個oracle的存儲過程:

 

Oracle存儲過程代碼  收藏代碼
  1. create or replace procedure p_test as  
  2. ware_code varchar2(14);  
  3. begin  
  4.   set transaction read only;  
  5.   select t.ware_code into ware_code from tp_area t where t.area_code = ’1’;  
  6.   dbms_output.put_line(ware_code);  
  7.   dbms_lock.sleep(10);–暫停10秒  
  8.   select t.ware_code into ware_code from tp_area t where t.area_code = ’1’;  
  9.   dbms_output.put_line(ware_code);  
  10. end p_test;  

 

 area_code是tp_area表的主鍵。在暫停的10秒鐘內,我們去更改area_code爲1的這條記錄中ware_code的值,並提交。

經過測試發現,兩次的輸出結果一致,這就證明了正確性。

ps:對於oracle中只讀事務的描述,推薦參考《Oracle 9i&10g編程藝術:深入數據庫體系結構》這本書!

 

但是我們要說明,並不是所有的數據庫都支持readonly事務。

 

2. 接下來我們討論jdbc中對於readonly事務的描述

 

jdk1.6中Java.sql.Connection接口中定義的方法

setReadOnly(boolean readOnly) throws SQLException的描述:將此連接設置爲只讀模式,作爲驅動程序啓用數據庫優化的提示。

例如:此連接設置爲只讀模式後,通知Oracle後,Oracle做自己的優化;通知DB2後,DB2做自己的優化等等,但是並不一定對於數據庫而言這就是readonly事務,此readonly並非彼readonly!

 

講到這裏,很多人可能並不信,難道setReadOnly爲true之後數據庫並不是以readonly事務執行的?

 

我們以實際的測試結果說話:

測試環境一:

測試環境代碼  收藏代碼
  1. 數據庫:Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production  
  2. oracle jdbc驅動版本:Oracle JDBC Driver version - 9.0.2.0.0 此驅動已經是很老的了,但是相信還是有很多項目還在用就是ojdbc14.jar。  
  3. jdk版本:jdk 1.6.0_20  

 

    測試代碼:

Java代碼  收藏代碼
  1. Class.forName(”oracle.jdbc.OracleDriver”);                                                                  
  2. Connection conn = DriverManager.getConnection(url, username, password);                                     
  3. conn.setAutoCommit(false);                                                                                  
  4. conn.setReadOnly(true);//啓動只讀模式                                                                                     
  5. PreparedStatement ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);  
  6. ResultSet rs = ps.executeQuery();                                                                           
  7. rs.next();                                                                                                  
  8. System.out.println(rs.getString(1));                                                                        
  9.                                                                                                             
  10. Thread.sleep(10000);//暫停10秒                                                                              
  11.                                                                                                             
  12. ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);                    
  13. rs = ps.executeQuery();                                                                                     
  14. rs.next();                                                                                                  
  15. System.out.println(rs.getString(1));                                                                        
  16.                                                                                                             
  17. ps.close();                                                                                                 
  18. conn.commit();                                                                                              

 

    在暫定的10秒鐘內,我通過pl/sql手動去修改了這條記錄中ware_code的值,並提交,那這兩次的輸出結果一樣嗎?

    測試的結果是:這兩次的輸出結果是一致的!

    咦,這不是對的嗎?oracle啓動了只讀事務,根據事務級都一致性的原則,兩次讀出來的應該是同一時間點的數據,應該一致啊!

    好,彆着急,我們再來看一個測試。

 

測試環境二:

Java代碼  收藏代碼
  1. 數據庫:Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production                       
  2. oracle jdbc驅動版本:Oracle JDBC Driver version - 11.2.0.1.0 這種從官方網站上面下載的最新的驅動ojdbc5.jar  
  3. jdk版本:jdk 1.6.0_20                                                                                      

 

   測試代碼(與上面的測試代碼一致):

Java代碼  收藏代碼
  1. Class.forName(”oracle.jdbc.OracleDriver”);                                                                  
  2. Connection conn = DriverManager.getConnection(url, username, password);                                     
  3. conn.setAutoCommit(false);                                                                                  
  4. conn.setReadOnly(true);                                                                                     
  5. PreparedStatement ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);  
  6. ResultSet rs = ps.executeQuery();                                                                           
  7. rs.next();                                                                                                  
  8. System.out.println(rs.getString(1));                                                                                                                                                            
  9. Thread.sleep(10000);//暫停10秒                                                                                                                                                                   
  10. ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);                    
  11. rs = ps.executeQuery();                                                                                     
  12. rs.next();                                                                                                  
  13. System.out.println(rs.getString(1));                                                                        
  14.                                                                                       
  15. ps.close();                                                                                                 
  16. conn.commit();                                                                                                                                

   在暫定的10秒鐘內,我同樣還是通過pl/sql手動去修改了這條記錄中ware_code的值,那這兩次的輸出結果一樣嗎?

 

   測試的結果是:這兩次的輸出結果是不一致的!

 

   ——————————————————————–分割線——————————————————————————–

 

   是不是不太敢相信自己的眼睛,我開始的時候也是很納悶,爲啥會這樣!!??

   我們先來看一下 Oracle JDBC Driver version - 9.0.2.0.0 版本的的api中的說明:

Java代碼  收藏代碼
  1. oracle.jdbc.driver.OracleConnection  
  2. public void setReadOnly(boolean value) throws java.sql.SQLException  
  3. Sets the Connection as read only if the value is true and the enables the Connection for writing (updat/delete/insert) with the value as false.  
 

   但是最直接有效的方式還是看源碼!

   好的,我們來看一下Oracle JDBC Driver version - 9.0.2.0.0 ojdbc14.jar這個版本的驅動源碼:

Java代碼  收藏代碼
  1. <span>//</span><span style=”white-space: normal;”>jdbc.oracle.driver.OracleConnection 的代碼片段</span><span>  
  2. public void setReadOnly(boolean flag)             
  3.     throws SQLException                           
  4. {                                                 
  5.     PreparedStatement preparedstatement = null;   
  6.     try                                           
  7.     {                                             
  8.         String s = null;                          
  9.         if (flag)                                 
  10.             s = ”SET TRANSACTION READ ONLY”;      
  11.         else                                      
  12.             s = ”SET TRANSACTION READ WRITE”;     
  13.         preparedstatement = prepareStatement(s);  
  14.         preparedstatement.execute();              
  15.     }                                             
  16.     finally                                       
  17.     {                                             
  18.         if (preparedstatement != null)            
  19.             preparedstatement.close();            
  20.     }                                             
  21.     m_readOnly = flag;                            
  22. }                                                 
  23. </span>  

 從源碼中我們可以很明顯的看出來當setReadOnly爲true時,究竟做了什麼事–>”SET TRANSACTION READ ONLY”,

 所以在此版本的jdbc實現中只要你setReadOnly爲true,則對於數據庫而言就是以只讀事務來執行。

 

 那Oracle JDBC Driver version - 11.2.0.1.0這個版本的驅動究竟怎麼回事呢?

 首先從這個版本的api中,我們已經找不到oracle.jdbc.driver.OracleConnection這個類了,

我們來看oracle.jdbc.OracleConnectionWrapper implements oracle.jdbc.OracleConnection

 雖然api中存在public void setReadOnly(boolean readOnly) throws java.sql.SQLException這個方法,但是沒有任何描述,很奇怪。

 

 我們來看一下源碼的片段:

 

Java代碼  收藏代碼
  1. protected oracle.jdbc.OracleConnection connection;  
  2. …..  
  3. public void setReadOnly(boolean flag)  
  4.         throws SQLException  
  5. {  
  6.         connection.setReadOnly(flag);  
  7. }  
  8.   
  9. //OracleConnection接口聲明:public interface OracleConnection extends Connection  

 

你能夠看出什麼來嗎? 是啊,根本就沒做什麼是嘛,對的,什麼事情都沒做!

ps:貌似此版本的驅動對於readonly屬性就是拋棄了,沒起作用!(沒有再做深入研究,不知這樣講是否正確)

 

甚至說我如下的代碼都可以執行通過:

 

Java代碼  收藏代碼
  1. Class.forName(”oracle.jdbc.OracleDriver”);                                              
  2. Connection conn = DriverManager.getConnection(url, username, password);                 
  3. conn.setAutoCommit(false);                                                              
  4. conn.setReadOnly(true);                                                                 
  5.                                                                                         
  6. PreparedStatement ps = conn.prepareStatement(”update tp_area t set t.ware_code =’t’”);  
  7. ps.executeUpdate();                                                                     
  8. ps.close();                                                                             
  9. conn.commit();                                                                          

 

 設置只讀後,我還能執行更新操作,並且運行測試都是Ok的!

 

但是如果我想要實現oracle的readonly事務該怎麼辦呢?

你可以從Oracle JDBC Driver version - 9.0.2.0.0 ojdbc14.jar源碼中得到思路,代碼如下:

Java代碼  收藏代碼
  1. Class.forName(”oracle.jdbc.OracleDriver”);                                                
  2. Connection conn = DriverManager.getConnection(url, username, password);                   
  3. conn.setAutoCommit(false);                                                                
  4. conn.setReadOnly(true);                                                                   
  5. //新增的兩行                                                                              
  6. PreparedStatement ps = conn.prepareStatement(”set transaction read only”);                
  7. ps.execute();                                                                             
  8.                                                                                           
  9. ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);  
  10. ResultSet rs = ps.executeQuery();                                                         
  11. rs.next();                                                                                
  12. System.out.println(rs.getString(1));                                                      
  13.                                                                                           
  14. Thread.sleep(10000);//暫停10秒                                                            
  15.                                                                                           
  16. ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);  
  17. rs = ps.executeQuery();                                                                   
  18. rs.next();                                                                                
  19. System.out.println(rs.getString(1));                                                      
  20.                                                                                           
  21. ps.close();                                                                               
  22. conn.commit();                                                                            

對的,就是設置手動顯示地 “set transaction read only”即可!ps:不知道還有沒有其他的方式?

 

經過上述分析後是不是豁然開朗的許多!

總結:1. 首先jdbc的規範中已經說明了readonly只是將此連接設置爲只讀模式,作爲驅動程序啓用數據庫優化的提示,並不一定以只讀事務執行!

 2.  對於oracle的jdbc驅動而言,不同版本的驅動會得出不同的結論!

但是我不清楚的是爲什麼oracle在後續驅動中,不支持了readonly事務了呢?處於性能的考慮?還是別的其他原因,有待求證!

 

3. SSH架構中spring的readonly事務

底層的驅動都可能不支持readonly事務,你說Spring的readonly事務能管用嗎?答案是很顯然的,當然不一定支持。

第二個帖子中的theone說的是對的。

引用代碼  收藏代碼
  1. “只讀事務”並不是一個強制選項,它只是一個“暗示”,提示數據庫驅動程序和數據庫系統,這個事務並不包含更改數據的操作,  
  2. 那麼JDBC驅動程序和數據庫就有可能根據這種情況對該事務進行一些特定的優化,比方說不安排相應的數據庫鎖,以減輕事務對數據庫的壓力,畢竟事務也是要消耗數據庫的資源的。 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章