資料庫事務簡介
1 資料庫事務
1.1 概念
事務是指作為單個邏輯工作單元執行的一系列操作,要麼完全地執行,要麼完全地不執行。
1.2 事務的特點
1.2.1 原子性
組成一個事務的多個數據庫操作是一個不可分割的原子單元,這些原子單元不可能在進行分割,事務中的所有操作執行成功整個事務才可以提交,如果某個操作失敗,那麼執行成功的操作也必須撤銷,讓資料庫恢復到原始狀態
1.2.2 隔離性
源於資料的併發操作。兩個事務擁有獨立的資料空間,彼此之間互不干擾。一個事務的狀態不會影響到另一個事務的執行。該特性需要事務隔離級別支援。也就是說我們可以通過設定不同的隔離級別來達到對應的隔離要求
1.2.3 一致性
是事務的根本,其他的3個特性都是為該特性服務的。只要事務提交成功,那麼資料應該和對應的業務規則保持一致,資料不會被破壞
1.2.4 永續性
一旦提交事務提交成功後,事務中涉及的資料都會儲存到資料庫中
資料庫採用重執行日誌來保證原子性,一致性和永續性。重執行日誌記錄了資料庫變化的每一個動作。資料庫在一個事務中執行一部分操作後發生錯誤退出,資料庫即可根據重執行日誌撤銷已經執行的操作。此外,當事務提交成功後,資料庫出現故障宕機,導致事務涉及的資料沒有及時儲存到資料庫中,當資料庫再次重啟時,資料庫可以根據重執行日誌,及時將提交成功但為更新到資料庫中的資料更新到資料庫中
2 資料庫併發引入的問題
2.1 髒讀
一個事務讀取到了另一個事務沒有提交的資料,此時基於該資料的操作都是不被認可的。
時間 | 事務A | 事務B |
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢餘額1000 | |
T4 | 取出500,餘額為500 | |
T5 | 查詢餘額1000(事務B未提交,事務A獲取的資料為髒讀) | |
T6 | 提交事務 | |
T7 | 存入200,餘額為1200 | |
T8 | 提交事務 |
2.2 不可重複讀
簡單的說就是同一個事務多次讀取資料後發現每次讀取的資料都不一樣。讀取過程中,其他的事務更新或者刪除資料。也就是說在重新讀取前,事務已經提交併將資料更新到資料庫中。讀取到了已經提交的更改資料。重點強調的是資料更新(刪除/更新)
髒讀和不可重複讀的區別:
髒讀:事務沒有提交
不可重複讀:分為如下兩個階段:1.第一次讀時,事務未提交,此時的表現和髒讀一致 2.第n次讀時,事務完成並提交即重新讀取前,事務結束並提交。
時間 | 事務A | 事務B |
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢餘額 | |
T4 | 查詢餘額1000 | |
T5 | 取出100,餘額為900 | |
T6 | 提交事務 | |
T7 | 查詢餘額900 |
2.3 幻想讀
一個事務讀取到了另一個事務插入的資料,讀取到了其他已經提交事務的新增資料。重點強調的是資料的新增
時間 | 事務A | 事務B |
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢總餘額1000 | |
T4 | 新開戶並存入100 | |
T5 | 提交事務 | |
T6 | 再次統計總查詢餘額1100(幻想讀) |
事務B進入了事務A的可視範圍,導致兩次統計的總餘額不一致。在幻想讀中存在的新資料的插入操作
2.4 第一類丟失更新
事務撤銷時,把已經提交的事務的更新資料覆蓋了,該類丟失可能造成很嚴重的後果。
時間 | 事務A | 事務B |
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢總餘額1000 | |
T4 | 查詢總餘額1000 | |
T5 | 存入100,餘額為1100 | |
T6 | 提交事務 | |
T7 | 取出100,餘額為900 | |
T8 | 撤銷事務 | |
T9 | 恢復餘額1000,更新丟失 |
2.5 第二類丟失更新
事務A覆蓋事務B已經提交的資料,導致事務B所做的操作丟失
時間 | 事務A | 事務B |
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢總餘額1000 | |
T4 | 查詢總餘額1000 | |
T5 | 取出100,餘額為900 | |
T6 | 提交事務 | |
T7 | 存入100 | |
T8 | 提交事務 | |
T9 | 餘額1100,更新丟失 |
3 資料庫鎖機制
資料庫通過鎖機制來解決併發帶來的問題。按照鎖定物件的不同可以將鎖分為行鎖定和表鎖定,行鎖定就是鎖定某一行記錄。表鎖定就是鎖定整張表。頁鎖定就是鎖定相鄰的一組記錄。
按照併發事務的關係可以分為共享鎖定和獨佔鎖定,共享鎖定會防止其他的獨佔鎖定但允許其他共享鎖定。獨佔鎖定即防止其他的獨佔鎖定也防止其他的共享鎖定。
更改資料時,資料庫必須在進行更改的行上施加行獨佔鎖定,[INSERT | UPDATE | DELETE | SELECT] FOR UPDATE語句都會隱式的採用必要的行鎖定。如下是ORACLE中常見的鎖定機制
3.1 行共享鎖定
屬於共享鎖定也屬於行鎖。該共享鎖定下可以對資料進行更改操作。但是防止其他會話獲取獨佔性資料表鎖定。允許行共享和行獨佔鎖定,還允許進行資料表的共享或者採用共享行獨佔鎖定。在oracle中使用者可以通過LOCK TABLE IN ROW SHARE MODE顯式獲得行獨佔鎖定。
3.2 行獨佔鎖定
屬於獨佔鎖定也屬於行鎖。通過[INSERT | UPDATE | DELETE]語句隱式獲得或者通過LOCK TABLE IN ROW EXCLUSIVE MODE語句顯示獲得。該中鎖定不允許其他回話獲取共享鎖定,共享行獨佔鎖定以及獨佔鎖定
3.2 表共享鎖定
屬於共享鎖定也屬於表鎖。通過LOCK TABLE IN SHARE MODE顯式獲得鎖定。防止其他會話獲得行獨佔鎖定(INSERT,UPDATE,DELETE),表共享行獨佔鎖定或者表獨佔鎖定,允許多個表共享鎖定以及行共享鎖定。該鎖定可以讓會話具有對錶事務級別的特性。當前事務沒有被提交或者回滾前,該鎖定不允許其他事務更新表中的任何資料
3.3 表共享行獨佔鎖定
通過LOCK TABLE IN SHARE ROW EXCLUSIVE MODE顯式獲得該鎖定。防止其他會話獲取一個表共享,行獨佔或者表獨佔鎖定。允許其他行共享鎖定。類似於表共享鎖定,只是一次只能對一張表放置一個表共享行獨佔鎖定。事務A擁有該鎖定,事務B可以執行查詢操作,如果事務B需要進行更新操作,需要等待獲得該鎖
3.4 表獨佔鎖定
通過LOCK TABLE IN EXCLUSIVE MODE顯式獲得,防止其他會話對該表的其他任何鎖定
3.5 悲觀鎖
就是每次獲取資料時都認為其他事務會修改資料,所以每次在獲取資料時都會將資料鎖定。鎖定後,其他的事務想要獲取資料時就會背阻塞。直到當前事務提交或者回滾。通俗的講其實就是在操作前先上鎖。行鎖,表鎖,共享鎖,排他鎖都屬於該悲觀鎖
3.6 行鎖
鎖定行。eg:select * from my_table where id = 1 for update.顯式鎖定id=1的行,也就是說如果要操作id=1的行需要等待當前事務提交或者回滾。
3.7 表鎖
鎖定表。select * from my_table for update
3.8 頁鎖
行鎖鎖定的表格中的某一行,表鎖鎖定的整張表,頁鎖定鎖定的是相鄰的一組記錄
3.9 共享鎖
又被稱為讀鎖。一個事務給操作該資料時候,其他事務只能讀取該資料,不能進行修改
3.10 排他鎖
又被稱為寫鎖,一個事務一旦採用該種方式鎖定資料,就不允許其他的事務讀寫該資料
3.11 樂觀鎖
預設事務不會修改資料,所以也就不用上鎖。只有在更新前判斷其他事務是否在此期間修改了資料,如果修改,交給業務層處理,常用的方式是使用版本戳。
事務A讀取資料並操作該資料此時資料的version=1
事務B讀取資料並操作該資料此時資料的version=1.
事務A操作完成提交事務發現version=1,提交成功並將version修改為2.
此時事務B操作完成提交事務發現version=2,於之前的version=1不一致,
說明有其他事務操作了資料,此時需要通知業務邏輯處理。
通常情況下,讀操作較多時,使用樂觀鎖,寫操作較多時,使用悲觀鎖。
4 事務隔離級別
隔離級別 | 髒讀 | 不可重複讀 | 幻想讀 | 第一類丟失 | 第二類丟失 |
READ UNCOMMITED | 允許 | 允許 | 允許 | 不允許 | 允許 |
READ COMMITED | 不允許 | 允許 | 允許 | 不允許 | 允許 |
REPEATABLE READ | 不允許 | 不允許 | 允許 | 不允許 | 不允許 |
SERIALIZABLE | 不允許 | 不允許 | 不允許 | 不允許 | 不允許 |
事務的隔離級別主要分4類。隔離級別越高資料庫處理併發的效能越低。一般READ COMMITED是資料庫預設的隔離級別
5 JDBC和事務
String URL="jdbc:mysql://127.0.0.1:3306/imooc?useUnicode=true&characterEncoding=utf-8"; String USER="root"; String PASSWORD="tiger"; Connection conn = null; Statement statement = null; try { //載入驅動 Class.forName("com.mysql.jdbc.Driver"); //獲得連線 conn = DriverManager.getConnection(URL, USER, PASSWORD); //設定事務是否自提交,fasle禁止自動提交 conn.setAutoCommit(false); //設定事務隔離級別 conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); //執行資料庫操作 statement = conn.createStatement(); statement.execute("select * from table"); statement.executeUpdate("UPDATE SQL Statement"); //提交事務 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { try { //回滾事務 conn.rollback(); //關閉資源 statement.close(); conn.close(); } catch (SQLException e1) { e1.printStackTrace(); } }
採用傳統的方式需要經過如下幾個步驟
1.定義資料庫URL以及使用者名稱密碼
2.獲取驅動
3.獲取資料庫連線
4.關閉事務自動提交
5.設定事務隔離級別
6.執行資料庫操作
7.沒有異常提交事務
8.發生異常回滾事務
9.不管是否發生異常,都需要關閉資源
如果我們進行多次資料庫的操作,希望在發生異常時我們依然將某些操作提交的資料庫中,此時我們可以使用儲存點來實現該功能
String URL="jdbc:mysql://127.0.0.1:3306/imooc?useUnicode=true&characterEncoding=utf-8"; String USER="root"; String PASSWORD="tiger"; Connection conn = null; Statement statement = null; Savepoint savepoint1 = null; Savepoint savepoint2 = null;try { //載入驅動 Class.forName("com.mysql.jdbc.Driver"); //獲得連線 conn = DriverManager.getConnection(URL, USER, PASSWORD); //設定事務是否自提交,fasle禁止自動提交 conn.setAutoCommit(false); //設定事務隔離級別 conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); //執行資料庫操作 statement = conn.createStatement(); statement.execute("select * from table"); //新增儲存點 savepoint1 = conn.setSavepoint("savepoint1");statement.execute("select * from table2"); //新增儲存點 savepoint2 = conn.setSavepoint("savepoint2");statement.executeUpdate("UPDATE SQL Statement"); //提交事務 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { try { //回滾事務 conn.rollback(savepoint2);//關閉資源 statement.close(); conn.close(); } catch (SQLException e1) { e1.printStackTrace(); } }發生異常時回滾到savepoint2,也就是說凡是在savepoint2之前的操作(兩次查詢)會背提交的資料庫而更新操作則被回滾