爲什麼需要鎖?
因爲數據庫要解決併發控制問題。在同一時刻,可能會有多個客戶端對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事務的定義!
第一個帖子中已經給出了結論:
- Oracle默認情況下保證了SQL語句級別的讀一致性,即在該條SQL語句執行期間,它只會看到執行前點的數據狀態,而不會看到執行期間數據被其他SQL改變的狀態。
- 而Oracle的只讀查詢(read-only transaction)則保證了事務級別的讀一致性,即在該事務範圍內執行的多條SQL都只會看到執行前點的數據狀態,
- 而不會看到事務期間的任何被其他SQL改變的狀態。
這個是Oracle對於只讀事務的描述,要保證事務級別的讀一致性有兩種方式,第一個帖子中也給出了結論:
我們來做一下測試(採用第二種方式):
編寫一個oracle的存儲過程:
- create or replace procedure p_test as
- ware_code varchar2(14);
- begin
- set transaction read only;
- select t.ware_code into ware_code from tp_area t where t.area_code = ’1’;
- dbms_output.put_line(ware_code);
- dbms_lock.sleep(10);–暫停10秒
- select t.ware_code into ware_code from tp_area t where t.area_code = ’1’;
- dbms_output.put_line(ware_code);
- 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事務執行的?
我們以實際的測試結果說話:
測試環境一:
- 數據庫:Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
- oracle jdbc驅動版本:Oracle JDBC Driver version - 9.0.2.0.0 此驅動已經是很老的了,但是相信還是有很多項目還在用就是ojdbc14.jar。
- jdk版本:jdk 1.6.0_20
測試代碼:
- Class.forName(”oracle.jdbc.OracleDriver”);
- Connection conn = DriverManager.getConnection(url, username, password);
- conn.setAutoCommit(false);
- conn.setReadOnly(true);//啓動只讀模式
- PreparedStatement ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);
- ResultSet rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- Thread.sleep(10000);//暫停10秒
- ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);
- rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- ps.close();
- conn.commit();
在暫定的10秒鐘內,我通過pl/sql手動去修改了這條記錄中ware_code的值,並提交,那這兩次的輸出結果一樣嗎?
測試的結果是:這兩次的輸出結果是一致的!
咦,這不是對的嗎?oracle啓動了只讀事務,根據事務級都一致性的原則,兩次讀出來的應該是同一時間點的數據,應該一致啊!
好,彆着急,我們再來看一個測試。
測試環境二:
- 數據庫:Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
- oracle jdbc驅動版本:Oracle JDBC Driver version - 11.2.0.1.0 這種從官方網站上面下載的最新的驅動ojdbc5.jar
- jdk版本:jdk 1.6.0_20
測試代碼(與上面的測試代碼一致):
- Class.forName(”oracle.jdbc.OracleDriver”);
- Connection conn = DriverManager.getConnection(url, username, password);
- conn.setAutoCommit(false);
- conn.setReadOnly(true);
- PreparedStatement ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);
- ResultSet rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- Thread.sleep(10000);//暫停10秒
- ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);
- rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- ps.close();
- conn.commit();
在暫定的10秒鐘內,我同樣還是通過pl/sql手動去修改了這條記錄中ware_code的值,那這兩次的輸出結果一樣嗎?
測試的結果是:這兩次的輸出結果是不一致的!
——————————————————————–分割線——————————————————————————–
是不是不太敢相信自己的眼睛,我開始的時候也是很納悶,爲啥會這樣!!??
我們先來看一下 Oracle JDBC Driver version - 9.0.2.0.0 版本的的api中的說明:
- oracle.jdbc.driver.OracleConnection
- public void setReadOnly(boolean value) throws java.sql.SQLException
- 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這個版本的驅動源碼:
- <span>//</span><span style=”white-space: normal;”>jdbc.oracle.driver.OracleConnection 的代碼片段</span><span>
- public void setReadOnly(boolean flag)
- throws SQLException
- {
- PreparedStatement preparedstatement = null;
- try
- {
- String s = null;
- if (flag)
- s = ”SET TRANSACTION READ ONLY”;
- else
- s = ”SET TRANSACTION READ WRITE”;
- preparedstatement = prepareStatement(s);
- preparedstatement.execute();
- }
- finally
- {
- if (preparedstatement != null)
- preparedstatement.close();
- }
- m_readOnly = flag;
- }
- </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這個方法,但是沒有任何描述,很奇怪。
我們來看一下源碼的片段:
- protected oracle.jdbc.OracleConnection connection;
- …..
- public void setReadOnly(boolean flag)
- throws SQLException
- {
- connection.setReadOnly(flag);
- }
- //OracleConnection接口聲明:public interface OracleConnection extends Connection
你能夠看出什麼來嗎? 是啊,根本就沒做什麼是嘛,對的,什麼事情都沒做!
ps:貌似此版本的驅動對於readonly屬性就是拋棄了,沒起作用!(沒有再做深入研究,不知這樣講是否正確)
甚至說我如下的代碼都可以執行通過:
- Class.forName(”oracle.jdbc.OracleDriver”);
- Connection conn = DriverManager.getConnection(url, username, password);
- conn.setAutoCommit(false);
- conn.setReadOnly(true);
- PreparedStatement ps = conn.prepareStatement(”update tp_area t set t.ware_code =’t’”);
- ps.executeUpdate();
- ps.close();
- conn.commit();
設置只讀後,我還能執行更新操作,並且運行測試都是Ok的!
但是如果我想要實現oracle的readonly事務該怎麼辦呢?
你可以從Oracle JDBC Driver version - 9.0.2.0.0 ojdbc14.jar源碼中得到思路,代碼如下:
- Class.forName(”oracle.jdbc.OracleDriver”);
- Connection conn = DriverManager.getConnection(url, username, password);
- conn.setAutoCommit(false);
- conn.setReadOnly(true);
- //新增的兩行
- PreparedStatement ps = conn.prepareStatement(”set transaction read only”);
- ps.execute();
- ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);
- ResultSet rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- Thread.sleep(10000);//暫停10秒
- ps = conn.prepareStatement(”select s.ware_code from tp_area s where s.area_code = ‘1’”);
- rs = ps.executeQuery();
- rs.next();
- System.out.println(rs.getString(1));
- ps.close();
- conn.commit();
對的,就是設置手動顯示地 “set transaction read only”即可!ps:不知道還有沒有其他的方式?
經過上述分析後是不是豁然開朗的許多!
總結:1. 首先jdbc的規範中已經說明了readonly只是將此連接設置爲只讀模式,作爲驅動程序啓用數據庫優化的提示,並不一定以只讀事務執行!
2. 對於oracle的jdbc驅動而言,不同版本的驅動會得出不同的結論!
但是我不清楚的是爲什麼oracle在後續驅動中,不支持了readonly事務了呢?處於性能的考慮?還是別的其他原因,有待求證!
3. SSH架構中spring的readonly事務
底層的驅動都可能不支持readonly事務,你說Spring的readonly事務能管用嗎?答案是很顯然的,當然不一定支持。
第二個帖子中的theone說的是對的。