本文包括:
1、事務概念
2、MySQL管理事務
3、JDBC控制事務進程
4、事務的特性(ACID)
5、事務的隔離級別
6、事務的丟失更新問題(lost update)
1、事務的概念
-
事務指邏輯上的一組操作,組成這組操作的各個單元,要不全部成功,要不全部不成功。
例如:A——B轉帳,對應于如下兩條sql語句update from account set money=money-100 where name=‘a’; update from account set money=money+100 where name=‘b’;
-
自動提交
MySQL 數據庫 默認情況下 一條SQL就是一個單獨事務,事務是自動提交的
Oracle 數據庫 默認情況下 事務不是自動提交 ,所有SQL都將處于一個事務中,你需要手動進行commit提交/rollback回滾
2、MySQL管理事務
在事務管理中執行sql,使用數據庫內臨時表保存,在沒有進行事務提交或者回滾,其它用戶無法看到事務操作結果的
SQL語言中只有DML才能被事務管理,如insert update delete
-
SQL語句:
start transaction 開啟事務 (所有對數據表增加、修改、刪除操作 臨時表進行)
rollback 回滾事務 (取消剛剛操作)
commit 提交事務 (確認剛才操作)
savepoint my_savepoint 保存點(回滾點,像打游戲時的存檔)
rollback to savepoint my_savepoint 回滾到指定的保存點
3、JDBC控制事務進程
當Jdbc程序向數據庫獲得一個Connection對象時,默認情況下這個Connection對象會自動向數據庫提交在它上面發送的SQL語句。若想關閉這種默認提交方式,讓多條SQL在一個事務中執行。
-
JDBC控制事務語句:
Connection.setAutoCommit(false); // 相當于SQL語句的start transaction Connection.rollback(); // 相當于SQL語句的rollback Connection.commit(); // 相當于SQL語句的commit Savepoint sp = conn.setSavepoint(); // 相當于SQL語句的savepoint my_savepoint Conn.rollback(sp); // 相當于SQL語句的rollback to savepoint my_savepoint
-
注意:
使用保存點進行事務控制時,回滾到指定保存點之后,必須要調用commit。
如果直接使用沒有保存點的rollback命令,那么事務中的所有保存點都將被忽略,并且撤銷整個事務(這就是普通rollback之后不需要調用commit的原因)
-
demo:
其中JDBCUtils文件源碼在另外一篇文章《Java與數據庫的橋梁——JDBC》
package cn.itcast.test; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Savepoint; import org.junit.Test; import cn.itcast.utils.JDBCUtils; public class TransferTest { @Test public void demo3() { // 創建person表,向表中插入20000條數據 ------ 如果插入過程中發生錯誤,則回滾到插入數據條數為1000的整數倍的時候 // PreparedStatement 批處理 Connection conn = null; PreparedStatement stmt = null; Savepoint savepoint = null; try { conn = JDBCUtils.getConnection(); // 開啟事務 conn.setAutoCommit(false); // 保存一次 savepoint = conn.setSavepoint(); String sql = "insert into person values(?,?)"; // 預編譯SQL stmt = conn.prepareStatement(sql); for (int i = 1; i <= 20000; i++) { stmt.setInt(1, i); stmt.setString(2, "name" + i); // 添加到批處理 stmt.addBatch(); if (i == 4699) { int d = 1 / 0; } // 每隔200向數據庫發送一次 if (i % 200 == 0) { stmt.executeBatch(); stmt.clearBatch(); } if (i % 1000 == 0) {// 1000整數倍 // 保存 回滾點 savepoint = conn.setSavepoint(); } } stmt.executeBatch();// 為了確保緩存sql都提交了 // 沒有錯誤 conn.commit(); } catch (Exception e) { // 回滾事務,回滾存儲點 try { conn.rollback(savepoint); conn.commit(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } finally { JDBCUtils.release(stmt, conn); } } @Test public void demo2() { // 模擬轉賬操作,使用事務管理 Connection conn = null; PreparedStatement stmt = null; try { conn = JDBCUtils.getConnection(); // 1、在連接獲得后,開啟事務 start transaction conn.setAutoCommit(false);// 關閉自動提交 // 沒有用事務管理,兩句sql 就是兩個事務 String sql1 = "update account set money = money - 100 where name='aaa'"; String sql2 = "update account set money = money +100 where name ='bbb'"; stmt = conn.prepareStatement(sql1); stmt.executeUpdate();// 執行 update操作 // int d = 1 / 0; stmt = conn.prepareStatement(sql2); stmt.executeUpdate();// 執行 update操作 // 2、兩句SQL 都執行成功,事務commit System.out.println("事務提交!"); conn.commit(); } catch (Exception e) { // 3、在執行轉賬過程中 發生錯誤,將兩句sql 進行回滾 System.out.println("事務回滾!"); try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); } finally { JDBCUtils.release(stmt, conn); } } @Test public void demo1() { // 模擬轉賬操作,先不使用事務管理 Connection conn = null; PreparedStatement stmt = null; try { conn = JDBCUtils.getConnection(); // 沒有用事務管理,兩句sql 就是兩個事務 String sql1 = "update account set money = money - 100 where name='aaa'"; String sql2 = "update account set money = money +100 where name ='bbb'"; stmt = conn.prepareStatement(sql1); stmt.executeUpdate();// 執行 update操作 int d = 1 / 0; stmt = conn.prepareStatement(sql2); stmt.executeUpdate();// 執行 update操作 } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.release(stmt, conn); } } }
4、事務的特性(ACID)
原子性(Atomicity)原子性是指事務是一個不可分割的工作單位,事務中的操作要么都發生,要么都不發生。
一致性(Consistency)事務前后數據的完整性必須保持一致。
隔離性(Isolation)事務的隔離性是指多個用戶并發訪問數據庫時,一個用戶的事務不能被其它用戶的事務所干擾,多個并發事務之間數據要相互隔離。
持久性(Durability)持久性是指一個事務一旦被提交,它對數據庫中數據的改變就是永久性的,接下來即使數據庫發生故障也不應該對其有任何影響。
企業開發中一定要保證事務原子性,事務最復雜問題都是由事務隔離性引起的
5、事務的隔離級別
-
不考慮事務隔離將引發哪些問題:臟讀、不可重復讀、虛讀
-
臟讀:一個事務讀取另一個事務未提交數據(數據庫隔離中最重要的問題)
這是非常危險的,假設A要在B的網店購物,A向B轉帳100元,對應sql語句如下所示
1.update account set money=money+100 while name=‘b’; 2.update account set money=money-100 while name=‘a’;
當第1條sql執行完,第2條還沒執行(A未提交時),如果此時B查詢自己的帳戶,就會發現自己多了100元錢,立即發貨,如果A等B發貨后再回滾,B就會損失貨物。
-
不可重復讀:一個事務讀取另一個事務未提交和已提交數據,兩次讀取結果不同(有時并不一定是壞問題)。
- 例如銀行想查詢A帳戶余額,第一次查詢A帳戶為200元,此時A向帳戶存了100元并提交了,銀行接著又進行了一次查詢,此時A帳戶為300元了。銀行兩次查詢不一致,可能就會很困惑,不知道哪次查詢是準的。
- 不可重復讀與臟讀的區別是:臟讀是讀取前一事務未提交的臟數據,不可重復讀是重新讀取了前一事務已提交的數據。
-
虛讀:一個事務讀取另一個事務插入數據,造成在一個事務中兩次讀取記錄條數不同(比不可重復讀危害性更低,很多數據庫不認為虛讀是問題)。
- 如丙存款100元未提交,這時銀行做報表統計account表中所有用戶的總額為500元,然后丙提交了,這時銀行再統計發現帳戶為600元了,造成虛讀同樣會使銀行不知所措,到底以哪個為準。
- 虛讀與不可重復讀的區別:不可重復讀讀取update的數據 ,虛讀讀取insert的數據。
-
-
數據庫共定義了四種隔離級別:
Serializable : 可解決 臟讀、不可重復讀、虛讀情況的發生。串行處理 ---解決三類問題
Repeatable read :可以解決 不可重復讀、臟讀,會發生虛讀 ------MySQL 默認級別
read committed : 可以 解決臟讀 ,會發生 不可重復讀、虛讀(讀已提交) -------Oracle默認級別
read uncommitted : 最低級別,會導致三類問題發生 (讀未提交)
Serializable > Repeatable read > read committed > read uncommitted
數據庫隔離問題危害:臟讀> 不可重復讀 > 虛讀,
安全級別越高,處理效率越低;安全級別越低,效率越高。所以一般數據庫把默認級別定位中間的兩級。
-
有關SQL語句:
set transaction isolation level XXX; 設置事務隔離級別
select @@tx_isolation; 查詢當前事務隔離級別
-
JDBC控制數據庫隔離級別:Connection類
Connection setTransactionIsolation(int level);
level字段摘要:
- static int TRANSACTION_NONE 指示事務不受支持的常量。
- static int TRANSACTION_READ_COMMITTED 指示不可以發生臟讀的常量;不可重復讀和虛讀可以發生。
- static int TRANSACTION_READ_UNCOMMITTED 指示可以發生臟讀 (dirty read)、不可重復讀和虛讀 (phantom read) 的常量。
- static int TRANSACTION_REPEATABLE_READ 指示不可以發生臟讀和不可重復讀的常量;虛讀可以發生。
- static int TRANSACTION_SERIALIZABLE 指示不可以發生臟讀、不可重復讀和虛讀的常量。
6、事務的丟失更新問題(lost update)
-
丟失更新問題描述:兩個事務查詢同一行,再先后更新這一行,但這些事務彼此之間都不知道其它事務進行的修改,所以因此第二個更改覆蓋了第一個修改。
-
首先介紹:在MySQL內部有兩種常用鎖:讀鎖和寫鎖
在mysql中默認情況下,在事務中當你修改數據,自動為數據加鎖,防止兩個事務同時修改數據 ---- 即讀鎖
事務和鎖和不可分開的,鎖一定是在事務中使用,當事務關閉鎖自動釋放。
-
讀鎖(共享鎖) 一張表可以添加多個讀鎖,如果表已經添加了讀鎖(該讀鎖不是當前事務添加的),則該表不可以修改
select * from account lock in share mode;
共享鎖非常容易發生死鎖(兩個事務都為同一張表添加了共享鎖,則該表在兩個事務中都不能修改)
-
寫鎖(排它鎖) 一張表只能加一個排它鎖,如果表已經添加了寫鎖,則該表不可以查詢,也不可以修改
select * from account for update ;
如果一張表想添加排它鎖,則之前表一定沒有加過共享鎖和排他鎖,否則無法添加排它鎖(排他鎖和其它共享鎖、排它鎖都具有互斥效果)
-
-
丟失更新問題的解決有兩種方法:
-
悲觀鎖( Pessimistic Locking )
select * from table lock in share mode(讀鎖、共享鎖) select * from table for update (寫鎖、排它鎖)
樂觀鎖( Optimistic Locking )
通過時間戳字段
-
-
悲觀鎖原理:使用數據庫內部鎖機制,進行table的鎖定,在A修改數據時,A就將數據鎖定,B此時無法進行修改 ----- 無法發生兩個事務同時修改
由1、2點可知,悲觀鎖可以使用排它鎖實現。
為什么叫悲觀鎖?
因為假設丟失更新會發生,所以是悲觀的。
-
樂觀鎖原理:使用的不是數據庫鎖機制,而是一個特殊時間戳標記字段,假設兩個事務查詢了同一個記錄,A事務修改了該記錄,則時間戳字段會更新,若B事務再修改該記錄,此時把之前查詢的時間戳字段與當前時間戳字段比較,得知數據是否發生了并發訪問。
表額外增加一個timestamp類型的字段,即時間戳字段,該字段在新增數據時可設為null,然后會自動生成當前時間,同時在修改該記錄時,也會自動更新為當前時間。