什麼是事務
- 是以一種可靠、一致的方式,訪問和運算元據庫中資料的程式單元
原則
- 原子性:一個事務要麼全部成功,要麼全部失敗
- 一致性:事務完成以後,狀態改變是一致的,一致性一般是通過結果來呈現的
- 隔離性:在不同事務試圖去操作同一份資料的時候,事務之間的隔離性
- 永續性:資料提交以後,事務操作的結果才會永久儲存到資料庫當中
範例 :a給b轉賬100元
一致性解讀:不會出現a賬戶-100而b賬戶沒有增加100的情況,整體的狀態是一致的,總的狀態不會平白無故地改變
隔離性解讀:在a給b轉賬的過程中,b進行查餘額的操作,那麼b得到到結果將由資料庫設定得資料庫隔離級別來決定。
使用sql進行資料管理
事務1:
START TRANSACTION;
UPDATE user_transaction set amount = amount -100 WHERE username = 'user1';
UPDATE user_transaction set amount = amount + 100 WHERE `username` = 'user2';
COMMIT
複製程式碼
事務2
SELECT * from user_transaction;
複製程式碼
事務3
START TRANSACTION;
SELECT * from user_transaction;
SELECT * FROM user_transaction WHERE username = 'user1';
COMMIT
複製程式碼
結果展示
- 在不修改資料庫預設隔離級別的情況下,只有當執行完commit之後,其它事務才能查詢到資料庫的資料庫的更新。
- 可重複讀的展示(可重複讀:一個事務中讀取到的資料和事務開啟的時刻是一致的) 事務3開啟事務以後,開啟事務1並執行事務1的第一條更新語句執行第一條查詢語句,得到結果如下 執行事務1的第二條更新語句並提交事務,然後執行事務3的第2條查詢語句 我們可以看到在可重讀的隔離級別下,一個事務內多次讀取的資料結果和事務開始時的結果是一致的,哪怕其它事務已經對原有資料進行更新並提交。
查詢資料庫中的設定的隔離級別,可以發現mysql預設的資料庫隔離級別是可重讀
select @@GLOBAL.tx_isolation,@@tx_isolation;
複製程式碼
修改資料庫當前事務隔離級別為髒讀
set session transaction isolation level read uncommited
複製程式碼
結果
- 事務1開啟事務並執行第一條更新語句,事務2可以看到事務1中執行的更新的資料,即使事務並沒有提交,即可以讀取到髒資料。
mysql資料庫的四種隔離級別
- 讀未提交
- 讀提交
- 可重讀
- 序列讀
jdbc操作事務
事務1:開啟事務->兩次更新->提交事務
public class JdbcTransaction {
public static void main(String args[]) throws SQLException {
Connection connection = getConn();
//關閉自動提交,相當於開啟一個事務
connection.setAutoCommit(false);
//減少賬戶餘額
String sql1 = "UPDATE user_transaction set amount = amount -100 WHERE username = ? ";
PreparedStatement ps1 = connection.prepareStatement(sql1);
ps1.setString(1, "user1");
ps1.executeUpdate();
//若丟擲異常,事務會回滾
//throwException();
//增加賬號餘額
String sql2 = "UPDATE user_transaction set amount = amount + 100 WHERE `username` = ?";
PreparedStatement ps2 = connection.prepareStatement(sql2);
ps2.setString(1, "user2");
ps2.executeUpdate();
// 提交事務
connection.commit();
ps1.close();
ps2.close();
}
private static Connection getConn() {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
Connection conn = null;
try {
Class.forName(driver); //classLoader,載入對應驅動
conn = (Connection) DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
private static void throwException() throws SQLException {
throw new SQLException();
}
複製程式碼
事務2: 開啟事務->進行查詢操作->根據查詢的結果再進行更新->提交
public class JdbcTransaction2 {
public static void main(String args[]) throws SQLException {
Connection connection = getConn();
// 關閉自動提交,相當於開啟一個事務
connection.setAutoCommit(false);
// 減少賬戶餘額,加悲觀鎖
String query1 = "select * from user_transaction for update";
PreparedStatement ps1 = connection.prepareStatement(query1);
ResultSet resultSet = ps1.executeQuery();
Integer myAmount = 0;
while (resultSet.next()){
String username = resultSet.getString(2);
Integer amount = resultSet.getInt(3);
System.out.println("username =" + username+ " amount = " + amount);
if (username.equals("user1")){
myAmount = amount;
}
}
// 根據查詢出來的結果去更新會有什麼問題?
/**
* 1. 開啟JdbcTransaction中的事務,執行更新操作但不commit
* 2. 本事務的更新操作將卡在更新資料之前,直到上一個事務提交,交出該資料鎖的許可權
* 3. 此時更新的資料將是根據前面查得的資料進行更新的,那麼此時更新的依據將會是舊的資料
*
* 解決:開啟事務,鎖住查詢出來的資料,若其它事務正在對本事務需要查詢的資料進行操作,那麼本事務等待直至
* 其它事務commit
*/
// 如果有其它事務正在操作這條資料,那麼此處將會等到其它事務提交以後才能繼續往下執行
// 根據mysql的內部機制,更新同一條資料的兩個事務不能同時執行,需要等待其中一個事務執行完
String sql2 = "UPDATE user_transaction set amount = ? WHERE username = ? ";
PreparedStatement ps2 = connection.prepareStatement(sql2);
ps2.setString(1,String.valueOf((myAmount+100)));
ps2.setString(2,"user1");
ps2.executeUpdate();
System.out.println("進行資料更新");
// 提交事務
connection.commit();
ps1.close();
ps2.close();
}
private static Connection getConn() {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
Connection conn = null;
try {
Class.forName(driver); //classLoader,載入對應驅動
conn = (Connection) DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
}
複製程式碼
資料庫:
樣例示範:執行事務1,但先不提交(斷點停留在commit( ) 處),開啟事務2,會發現事務2會停留在第二條sql(更新資料的sql)前,原因是事務1正在對同一份資料進行更新,事務2無法獲取到資料的鎖;這時提交事務1,事務2也隨之提交。這時會出現的問題是事務2中是根據舊資料為依據進行更新的,這種情況在生產中是不允許出現的。 解決方式:
- 對事務2中查詢出來的資料進行加鎖,這裡要注意的是加鎖的資料一定要是我們需要查詢的指定資料,所以這裡一定要加上where條件,否則有可能導致鎖全表,這樣將給系統效能帶來很大的影響。開啟事務1以後,事務2直到事務1提交以後才拿到查詢結果,因為要先獲取該行資料的鎖,這樣則不會出現讀舊資料去更新的情況。