問題引入:
同一個應用程序中的多個事務或不同應用程序中的多個事務在同一個數據集上併發執行時, 可能會出現許多意外的問題,這些問題可分爲如下三種類型:
髒讀(Drity Read): 已知有兩個事務A和B, A讀取了已經被B更新但還沒有被提交的數據,之後,B回滾事務,A讀取的數據就是髒數據(即修改之前的數據)。
模擬情景:
場景:公司發工資了,領導把5000元打到Tom的賬號上,但是該事務並未提交,而Tom正好去查看賬戶,發現工資已經到賬,賬戶多了5000元,非常高興,可是不幸的是,領導發現發給Tom的工資金額不對,是2000元,於是迅速回滾了事務,修改金額後,將事務提交,Tom再次查看賬戶時發現賬戶只多了2000元,Tom空歡喜一場。
分析:上述情況即爲髒讀,兩個併發的事務:“事務B:領導給Tom發工資”、“事務A:Tom查詢工資賬戶”,事務A讀取了事務B尚未提交的數據。
事務B:
import java.sql.*;
public class Boss {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test";
connection = DriverManager.getConnection(url, "root", "root");
connection.setAutoCommit(false);
statement = connection.createStatement();
String sql = "update account set balance=balance+5000 where card_id='6226090219290000'";
statement.executeUpdate(sql);
Thread.sleep(30000);//30秒後發現工資發錯了
connection.rollback();
sql = "update account set balance=balance+2000 where card_id='6226090219290000'";
statement.executeUpdate(sql);
connection.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
//釋放資源
}
}
}
事務A:
import java.sql.*;
public class Employye {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test";
connection = DriverManager.getConnection(url, "root", "root");
statement = connection.createStatement();
String sql = "select balance from account where card_id='6226090219290000'";
resultSet = statement.executeQuery(sql);
if(resultSet.next()) {
System.out.println(resultSet.getDouble("balance"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//釋放資源
}
}
}
不可重複讀(Non-repeatable read):已知有兩個事務A和B,A 多次讀取同一數據,B 在A多次讀取的過程中對數據作了修改並提交,導致A多次讀取同一數據時,結果不一致,例子:
模擬情景:
場景:Tom拿着工資卡去消費,酒足飯飽後在收銀臺買單,服務員告訴他本次消費1000元,Tom將銀行卡給服務員,服務員將銀行卡插入POS機,POS機讀到卡里餘額爲3000元,就在Tom磨磨蹭蹭輸入密碼時,他老婆以迅雷不及掩耳盜鈴之勢把Tom工資卡的3000元轉到自己賬戶並提交了事務,當Tom輸完密碼並點擊“確認”按鈕後,POS機檢查到Tom的工資卡已經沒有錢,扣款失敗,Tom十分納悶,明明卡里有錢,於是懷疑POS有鬼,和收銀小姐姐大打出手,300回合之後終因傷勢過重而與世長辭,Tom老婆痛不欲生,鬱鬱寡歡,從此走上了不歸路......
分析:上述情況即爲不可重複讀,兩個併發的事務,“事務A:POS機扣款”、“事務B:Tom的老婆網上轉賬”,事務A事先讀取了數據,事務B緊接了更新數據並提交了事務,而事務A再次讀取該數據扣款時,數據已經發生了改變。
場景:Tom拿着工資卡去消費時,一旦POS機讀取工資卡信息(即事務開始),Tom老婆即便進行了轉賬,待Tom輸入密碼並點擊“確認”按鈕後,POS機檢查到Tom工資卡上餘額沒有變化,最終扣款成功。
分析:上述情況即爲重複讀
事務A:
import java.sql.*;
public class Machine {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
double sum=1000;//消費金額
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test";
connection = DriverManager.getConnection(url, "root", "root");
connection.setAutoCommit(false);
statement = connection.createStatement();
String sql = "select balance from account where card_id='6226090219290000'";
resultSet = statement.executeQuery(sql);
if(resultSet.next()) {
System.out.println("餘額:"+resultSet.getDouble("balance"));
}
System.out.println("請輸入支付密碼:");
Thread.sleep(30000);//30秒後密碼輸入成功
resultSet = statement.executeQuery(sql);
if(resultSet.next()) {
double balance = resultSet.getDouble("balance");
System.out.println("餘額:"+balance);
if(balance<sum) {
System.out.println("餘額不足,扣款失敗!");
return;
}
}
sql = "update account set balance=balance-"+sum+" where card_id='6226090219290000'";
statement.executeUpdate(sql);
connection.commit();
System.out.println("扣款成功!");
} catch (Exception e) {
e.printStackTrace();
} finally {
//釋放資源
}
}
}
事務B:
import java.sql.*;
public class Wife {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
try {
double money=3000;//轉賬金額
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test";
connection = DriverManager.getConnection(url, "root", "root");
connection.setAutoCommit(false);
statement = connection.createStatement();
String sql = "update account set balance=balance-"+money+" where card_id='6226090219290000'";
statement.executeUpdate(sql);
sql = "update account set balance=balance+"+money+" where card_id='6226090219299999'";
statement.executeUpdate(sql);
connection.commit();
System.out.println("轉賬成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
//釋放資源
}
}
}
幻讀(Phantom Read): 已知有兩個事務A和B,A從一個表中讀取了數據,然後B在該表中插入了一些新數據,導致A再次讀取同一個表, 就會多出幾行,簡單地說,一個事務中先後讀取一個範圍的記錄,但每次讀取的紀錄數不同,稱之爲幻象讀,例子:
模擬情景:
場景:Tom的老婆工作在銀行部門,她時常通過銀行內部系統查看Tom的工資卡消費記錄。2019年5月的某一天,她查詢到Tom當月工資卡的總消費額(select sum(amount) from record where card_id='6226090219290000' and date_format(create_time,'%Y-%m')='2019-05')爲80元,Tom的老婆非常吃驚,心想“老公真是太節儉了,嫁給他真好!”,而Tom此時正好在外面胡吃海塞後在收銀臺買單,消費1000元,即新增了一條1000元的消費記錄並提交了事務,沉浸在幸福中的老婆查詢了Tom當月工資卡消費明細(select amount from record where card_id='6226090219290000' and date_format(create_time,'%Y-%m')='2019-05')一探究竟,可查出的結果竟然發現有一筆1000元的消費,Tom的老婆瞬間怒氣沖天,外賣訂購了一個大號的榴蓮,傍晚降臨,Tom生活在了水深火熱之中,只感到膝蓋針扎的痛......
分析:上述情況即爲幻讀,兩個併發的事務,“事務A:獲取事務B消費記錄”、“事務B:添加了新的消費記錄”,事務A獲取事務B消費記錄時數據多出了一條。
場景:教師A執行SQL語句將數據庫中所有學生的成績從具體分數改爲ABCDE等級制,該SQL語句執行後事務提交前,教師B插入了一條具體分數的記錄並提交了事務,教師A事務提交併執行查詢SQL語句,此時教師A發現還有一條記錄沒有改過來,就好像發生了幻覺一樣。
事務A:
import java.sql.*;
public class Bank {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test";
connection = DriverManager.getConnection(url, "root", "root");
connection.setAutoCommit(false);
statement = connection.createStatement();
String sql = "select sum(amount) total from record where card_id='6226090219290000' and date_format(create_time,'%Y-%m')='2019-05'";
resultSet = statement.executeQuery(sql);
if(resultSet.next()) {
System.out.println("總額:"+resultSet.getDouble("total"));
}
Thread.sleep(30000);//30秒後查詢2019年5月消費明細
sql="select amount from record where card_id='6226090219290000' and date_format(create_time,'%Y-%m')='2019-05'";
resultSet = statement.executeQuery(sql);
System.out.println("消費明細:");
while(resultSet.next()) {
double amount = resultSet.getDouble("amount");
System.out.println(amount);
}
connection.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
//釋放資源
}
}
}
事務B:
import java.sql.*;
public class Husband {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
try {
double sum=1000;//消費金額
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/test";
connection = DriverManager.getConnection(url, "root", "root");
connection.setAutoCommit(false);
statement = connection.createStatement();
String sql = "update account set balance=balance-"+sum+" where card_id='6226090219290000'";
statement.executeUpdate(sql);
sql = "insert into record (id,card_id,amount,create_time) values (3,'6226090219290000',"+sum+",'2019-05-19');";
statement.executeUpdate(sql);
connection.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
//釋放資源
}
}
}
小結:不可重複讀的和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。解決不可重複讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表。
解決方案
根據實際需求,通過設置數據庫的事務隔離級別可以解決多個事務併發情況下出現的髒讀、不可重複讀和幻讀問題,數據庫事務隔離級別由低到高依次爲Read uncommitted、Read committed、Repeatable read和Serializable等四種。數據庫不同,其支持的事務隔離級別亦不相同:MySQL數據庫支持上面四種事務隔離級別,默認爲Repeatable read;Oracle 數據庫支持Read committed和Serializable兩種事務隔離級別,默認爲Read committed。
1、Read uncommitted(讀未提交):可能出現髒讀、不可重複讀和幻讀。
2、Read committed(讀提交):可以避免髒讀,但可能出現不可重複讀和幻讀。大多數數據庫默認級別就是Read committed,比如Sql Server數據庫和Oracle數據庫。注意:該隔離級別在寫數據時只會鎖住相應的行。
3、Repeatable read(重複讀):可以避免髒讀和不可重複讀,但可能出現幻讀。注意:①、事務隔離級別爲可重複讀時,如果檢索條件有索引(包括主鍵索引)的時候,默認加鎖方式是next-key 鎖;②、如果檢索條件沒有索引,更新數據時會鎖住整張表。一個間隙被事務加了鎖,其他事務是不能在這個間隙插入記錄的,這樣可以防止幻讀。
4、Serializable(序列化):可以避免髒讀、不可重複讀和幻讀,但是併發性極低,一般很少使用。注意:該隔離級別在讀寫數據時會鎖住整張表。
說明:√表示可能出現,×表示不會出現
注注意意注意:隔離級別越高,越能保證數據的完整性和一致性,但是對併發性能的影響也越大。
MySQL事務隔離級別
查看:MySQL數據庫支持Read uncommitted、Read committed、Repeatable read和Serializable四種事務隔離級別,默認爲Repeatable read,可以通過如下語句查看MySQL數據庫事務隔離級別:
select @@global.tx_isolation,@@tx_isolation;
修改:MySQL數據庫事務隔離級別的修改分爲全局修改和當前session修改,具體修改方法如下:
1、全局修改
①、在my.ini配置文件最後加上如下配置:
#可選參數有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = READ-UNCOMMITTED
②、重啓MySQL服務
2、當前session修改,登錄MySQL數據庫後執行如下命令:
set session transaction isolation level read uncommitted;