根據實際需求,通過設置數據庫的事務隔離級別可以解決多個事務併發情況下出現的髒讀、不可重複讀和幻讀問題,數據庫事務隔離級別由低到高依次爲Read uncommitted、Read committed、Repeatable read和Serializable等四種。數據庫不同,其支持的事務隔離級別亦不相同:MySQL數據庫支持上面四種事務隔離級別,默認爲Repeatable read;Oracle 數據庫支持Read committed和Serializable兩種事務隔離級別,默認爲Read committed。
Serializable(序列化):可以避免髒讀、不可重複讀和幻讀,但是併發性極低,一般很少使用。
注意:該隔離級別在讀寫數據時會鎖住整張表。
數據庫事務隔離級別設置爲Serializable(序列化):在並重啓MySQL服務。
髒讀
已知有兩個事務A和B, A讀取了已經被B更新但還沒有被提交的數據,之後,B回滾事務,A讀取的數據就是髒數據。
場景:公司發工資了,領導把5000元打到Tom的賬號上,但是該事務並未提交,而Tom正好去查看賬戶,發現工資已經到賬,賬戶多了5000元,非常高興,可是不幸的是,領導發現發給Tom的工資金額不對,是2000元,於是迅速回滾了事務,修改金額後,將事務提交,Tom再次查看賬戶時發現賬戶只多了2000元,Tom空歡喜一場,從此鬱鬱寡歡,走上了不歸路…...
分析:上述情況即爲髒讀,兩個併發的事務:“事務B:領導給Tom發工資”、“事務A:Tom查詢工資賬戶”
create table account( id int(36) primary key comment '主鍵', card_id varchar(16) unique comment '卡號', name varchar(8) not null comment '姓名', balance float(10,2) default 0 comment '餘額' )engine=innodb; insert into account (id,card_id,name,balance) values (1,'6226090219290000','Tom',1000);
數據庫中數據顯示:
代碼如下:
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 { //釋放資源 } } }
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 { //釋放資源 } } }
演示流程:
先執行Boss類中main方法——>再執行Employye類中main方法——>Boss類中main方法執行完畢——>再執行Employye類中main方法——>觀察Employye類中main方法輸出
先執行Boss類中main方法——>再執行Employye類中main方法----運行結果:1000——>Boss類中main方法執行完畢——>再執行Employye類中main方法——>觀察Employye類中main方法輸出----> 30秒線程阻塞後,運行結果爲3000兩個併發的事務:“事務B:領導給Tom發工資”、“事務A:Tom查詢工資賬戶”,在-SERIALIZABLE(序列化)級別下,事務A沒有讀取了事務B尚未提交的數據。 此時並沒有出現髒讀的情況,所以當事務的隔離級別爲-SERIALIZABLE(序列化)的時候可以避免髒讀。
不可重複讀
已知有兩個事務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再次讀取該數據扣款
create table account( id int(36) primary key comment '主鍵', card_id varchar(16) unique comment '卡號', name varchar(8) not null comment '姓名', balance float(10,2) default 0 comment '餘額' )engine=innodb; insert into account (id,card_id,name,balance) values (1,'6226090219290000','Tom',3000); insert into account (id,card_id,name,balance) values (2,'6226090219299999','Lily',0);
數據庫中結果顯示:
代碼如下:
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 { //釋放資源 } } }
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 { //釋放資源 } } }
演示流程:
先執行Machine類中main方法——>再執行Wife類中main方法——>觀察Machine類中main方法輸出:兩個併發的事務,“事務A:POS機扣款”、“事務B:Tom的老婆網上轉賬”,事務A事先讀取了數據,事務B緊接了更新數據並提交了事務,而事務A再次讀取該數據扣款時,數據沒有發生了改變。 上面的結果顯示並沒有出現不可重複讀,所有當事務的隔離級別設置爲-SERIALIZABLE(序列化)的時候可以避免不可重複讀,妻子轉賬成功了,Tom也成功的支付了費用,但是卡中只有3000元,查看數據庫中的數據:
Tom賬戶中的金額變成了負數這是因爲:在數據庫事務隔離級別爲-SERIALIZABLE(序列化)的情況下,POS機讀取工資卡信息(此時Tom工資卡餘額3000元),Tom老婆進行了轉賬並提交了事務(此時Tom工資卡餘額0元),Tom輸入密碼並點擊“確認”按鈕,POS機再次讀取工資卡信息發現餘額確實沒有變化,但要最後一次讀取的數據並不是來自於數據庫物理磁盤,而是來自於緩存上的數據——MySQL數據庫中“可重複讀的隔離級別下使用了MVCC(https://blog.csdn.net/whoamiyang/article/details/51901888)select操作不會更新版本號,是快照讀(歷史版本);insert、update和delete會更新版本號,是當前讀(當前版本)”
幻讀
已知有兩個事務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消費記錄
create table account( id int(36) primary key comment '主鍵', card_id varchar(16) unique comment '卡號', name varchar(8) not null comment '姓名', balance float(10,2) default 0 comment '餘額' )engine=innodb; insert into account (id,card_id,name,balance) values (1,'6226090219290000','Tom',3000); create table record( id int(36) primary key comment '主鍵', card_id varchar(16) comment '卡號', amount float(10,2) comment '金額', create_time date comment '消費時間' )engine=innodb; insert into record (id,card_id,amount,create_time) values (1,'6226090219290000',37,'2019-05-01'); insert into record (id,card_id,amount,create_time) values (2,'6226090219290000',43,'2019-05-07');
account數據庫表中的數據:
record數據庫表中的數據:
代碼如下:
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 { //釋放資源 } } }
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 { //釋放資源 } } }
演示流程:
先執行Bank類中main方法(Tom當月工資卡的總消費額爲80元)——>再執行Husband類中main方法(此時Tom花費了1000元)——>觀察Bank類中main方法輸出:30秒線程阻塞後打印的消費明細:
此時數據庫中數據也依舊沒改變。
上述的場景,並沒有出現幻讀的情況,所以把數據庫的事務隔離級別設置爲SERIALIZABLE的時候可以避免幻讀的問題,但是這樣效率就會大打折扣。