JDBC之事務詳解

在前面的文章中,我們學習瞭如何執行增刪改查操作,也學習瞭如何來調用存儲過程。今天我們就來學習下如何在JDBC中使用事務。

​在某些情況下,某個任務執行成功的必要條件爲多個子任務全部執行成功,那我們我們就可以稱這多個子任務爲一個事務。我們以轉賬爲例:比如用戶A要從自己的賬戶中轉出1000元到用戶B的賬戶中,我們來看看這個轉賬操作需要執行什麼流程?

資源分配圖

​ 如上圖,當圖中的1、2、3、4和其他省略的操作全部都完成後,這次轉賬操作就成功了,A、B的賬戶有對應的金額變更和流水記錄。但是如果,在執行的過成中,A用戶這邊已經扣減了金額,但是由於網絡原因或者異常等原因B賬戶金額並沒能增加成功,那我們就需要將A、B賬戶還原爲此次轉賬操作前的狀態,這樣要怎麼處理?如果在A賬戶上的操作已經提交數據庫了,即數據庫中的賬戶信息已經update了,那麼這時就需要手動的再將金額變回來,然後在處理一堆的流水、衝正、回退等等等。那這樣子搞的話,想想異常處理中要回退的邏輯,簡直是要瘋的節奏。

資源分配圖

​如果所有的異常導致的數據庫中數據不一致都需要自己動手寫回退代碼的話,我想不用到30歲,我就有了大佬的髮型。幸好,我們有保證數據一致(和分佈式沒半毛錢關係,不要誤解)的神兵利器-----事務

1.事務的概念

​ 事務(Transaction),字面理解,一般是指要做的或所做的事情。

​ 在計算機術語中是指訪問並可能更新數據庫中各種數據項的一個程序執行單元(unit)。事務通常由高級數據庫操縱語言或編程語言(如SQL,C++或Java)書寫的用戶程序的執行所引起,並用形如begin transactionend transaction語句(或函數調用)來界定。事務由事務開始(begin transaction)和事務結束(end transaction)之間執行的全體操作組成。

​ 上述解釋來自百度百科,如果你學過操作系統,可以將事務理解成原語,事務中的每件不可分割的子任務可以視爲一條指令

​ 在我們今天要講的數據庫中的事務,就可以理解爲一條或一組SQL語句,這個事務成功的執行的必要條件就是其中的SQL全部執行成功,如果其中的任一SQL執行失敗(沒有達到預期效果),則此次事務執行失敗,所有的SQL都不會對數據庫中的數據產生變更。

​ 事務具有4個屬性:原子性、一致性、隔離性、持久性。這四個屬性通常稱爲ACID特性

  • 原子性(Atomicity):一個事務是一個不可分割的工作單位,事務中包括的操作要麼都做,要麼都不做;
  • 一致性(Consistency):事務必須是使數據庫從一個一致性狀態變到另一個一致性狀態。一致性與原子性是密切相關的;
  • 隔離性(Isolation):一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的數據對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾;
  • 持久性(Durability):持久性也稱永久性(Permanence),指一個事務一旦提交,它對數據庫中數據的改變就應該是永久性的。

​ 對於其中的隔離性,數據庫服務器有時會爲了提供更好的處理併發能力,會犧牲一定的隔離性。這也是正確性和性能之間的對抗。如MySQL默認的隔離級別爲READ COMMITTED,即讀提交,可以讀取其他事務已經提交的內容,可以避免髒讀,但是無法避免重複度和幻讀

2.JDBC中事務的相關方法

​ 下面我們來看下,當使用JDBC操作數據庫時,要如何使用事務。這裏,我們要新介紹幾個Connection接口中的幾個方法,如下表所示:

方法名 功能描述
void setAutoCommit(boolean autoCommit) 設置此連接的自動提交模式;默認爲自動提交
void commit() 使自上一次提交/回退以來的所有更改永久生效,並釋放此Connection對象當前持有的所有數據庫鎖。
void rollback() 撤消在當前事務中所做的所有更改,並釋放此Connection對象當前持有的所有數據庫鎖
void rollback(Savepoint savepoint) 撤消設置給定的Savepoint對象之後所做的所有更改

當Connection自動提交模式爲true的時候,即默認值,其所有SQL語句將作爲單個事務執行並提交,每次SQL執行時會默認提交,不需手動觸發;當自動提交模式爲false時,其SQL語句會分組爲事務(commit與commit之間的SQL或commit與rollback之間的SQL),這些事務通過調用commit方法提交,或調用rollback方法來進行回滾。

3.一個簡單的示例

​ 我們在JDBC中使用事務可以參考如下模板,其中的JDBCUtil代碼參考上文

public void transactionTemplate() throws ClassNotFoundException, SQLException {
  Connection connection = null;
  Statement statement = null;
  ResultSet resultSet = null;
  try {
    // 獲取數據庫連接
    connection = JDBCUtil.getConnection();
    // 設置自動提交的模式爲false
    connection.setAutoCommit(false);
    // 創建Statement對象 或 prepareStatement對象
    statement = connection.createStatement();
    statement.execute("sql 1");
    // ...
    statement.execute("sql n");
    //提交上面sql 1-sql n所有的SQL語句
    connection.commit();
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
    throw e;
  } catch (SQLException e) {
    e.printStackTrace();
    //SQL執行異常,撤銷此次事務中的所有SQL
    connection.rollback();
    throw e;
  } finally {
    // 如果此連接不釋放的話,則將自動提交模式改回true
    connection.setAutoCommit(true);
    JDBCUtil.release(resultSet, statement, connection);
  }
}

​ 爲了模擬事務操作,我們新建一張賬戶表,用於存儲用戶的賬戶金額(刪減版),SQL如下:

CREATE TABLE `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(15) NOT NULL COMMENT '賬戶名',
  `balance` bigint(20) DEFAULT '0' COMMENT '賬戶餘額,單位:分',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

-- insert
INSERT INTO `java_web`.`account`(`id`, `name`, `balance`) VALUES (1, 'A', 2000);
INSERT INTO `java_web`.`account`(`id`, `name`, `balance`) VALUES (2, 'B', 500);

​ 我們在表中插入兩條數據來模擬我們在文章開始時的轉賬操作。對應的Java代碼如下:

/**
	 * 轉賬操作
	 * @param from 發起人賬戶名,扣減賬戶
	 * @param to 收款人賬戶名,增加賬戶
	 * @param balance 轉賬金額,單位分
	 * @return
	 * @throws ClassNotFoundException
	 * @throws SQLException
	 */
	public boolean transferOperation(String from, String to, int balance) throws ClassNotFoundException, SQLException {
		Connection connection = null;
		Statement statement = null;
		ResultSet resultSet = null;
		try {
			// 獲取數據庫連接
			connection = JDBCUtil.getConnection();
			// 設置自動提交的模式爲false
			connection.setAutoCommit(false);
			// 創建Statement對象 或 prepareStatement對象
			statement = connection.createStatement();
			//A賬戶扣減對應金額 需保證賬戶餘額大於扣減金額
			int result1 = statement.executeUpdate("update account set balance = balance - " + 
					balance + " where name = '" + from + "' and balance >= " + balance);
			// B賬戶增加對應金額
			int result2 = statement.executeUpdate("update account set balance = balance + " + 
					balance + " where name = '" + to + "'");
      		// 判斷扣減和增加是否都執行成功
			if (result1 == 0 || result2 == 0) {
				throw new SQLException("事務執行失敗");
			}
			// 提交上面所有的SQL語句
			connection.commit();
			return true;
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
			throw e;
		} catch (SQLException e) {
			e.printStackTrace();
			// SQL執行異常,撤銷此次事務中的所有SQL
			connection.rollback();
			throw e;
		} finally {
			// 如果此連接不釋放的話,則將自動提交模式改回true
			connection.setAutoCommit(true);
			JDBCUtil.release(resultSet, statement, connection);
		}
	}
	

​ 執行測試代碼transferOperation("A", "B", 1000)後,數據庫中的數據如下圖所示:

資源分配圖

​ 當我們再次執行測試代碼transferOperation("A", "B", 2000)後,我們可以看到控制檯拋出如下錯誤,這裏是因爲上次轉賬操作,是的A賬戶的餘額只有1000(單位:分)了,當再次轉賬2000時,A賬戶的餘額不足了,因此A賬戶扣減SQL影響的行數爲0,而B賬戶增加操作執行成功了,但是因爲這個事務中有SQL執行失敗,因此我們將異常拋出,並rollback此次事務。

資源分配圖

​ 再次去數據中查看account表,發現A、B賬戶的餘額都未發生變化。

注意rollback應當發生在commit操作之前,因爲commit之後的,此次事務對數據庫的改變是無法被撤銷的,也就是事務的持久性。對此,我們可以做一個小的實驗,我們將上面的函數做下小小的改動,代碼如下所示:

//...
// 提交上面所有的SQL語句
connection.commit();
// 判斷扣減和增加是否都執行成功
if (result1 == 0 || result2 == 0) {
  throw new SQLException("事務執行失敗");
}
//...

​ 執行測試代碼transferOperation("A", "B", 2000)後,控制檯還是拋出了異常,這裏好像和之前(異常拋出在前)沒啥區別哦,先別急,我們在去數據庫中確認下,結果如下圖所示:

資源分配圖

​ 哇🤩,B賬戶憑空多了1000哦,如果這種錯誤發生在生產上,那要恭喜你,你有張機票待簽收。

資源分配圖

4.總結

​本文是對JDBC中如何使用事務的簡單示例,不管是Mybatis還是Hibernate,都是支持事務的。不過事務的執行速度相比較原有SQL會稍慢一些,會產生更多的日誌,執行更新時佔用更多的內存等。在許多公司中,會使用另一種方式來代替事務的功能,那就是失敗補償機制,當執行過程中發生異常時,會有重試機制將此次調用重新執行,當然這也需要接口保證冪等性。因此事務的使用還是要根據自己公司的要求和具體的業務邏輯。

參考閱讀:

  1. MySQL的四種事務隔離級別

​又到了分隔線以下,本文到此就結束了,本文內容全部都是由博主自己進行整理並結合自身的理解進行總結,如果有什麼錯誤,還請批評指正。

​Java web這一專欄會是一個系列博客,喜歡的話可以持續關注,如果本文對你有所幫助,還請還請點贊、評論加關注。

​有任何疑問,可以評論區留言。

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