首先,我們先設置MySQL事務隔離級別為REPEATABLE-READ
- 在my.ini配置文件最后加上如下配置
#可選參數有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = REPEATABLE-READ
- 重啟MySQL服務
1、臟讀
提出問題
同一個應用程序中的多個事務或不同應用程序中的多個事務在同一個數據集上並發執行時, 可能會出現許多意外的問題。
例如: 已知有兩個事務A和B, B讀取了已經被A更新但還沒有被提交的數據,之后,A回滾事務,B讀取的數據就是臟數據。
場景:
Tom的賬戶money=0,公司發工資把5000元打到Tom的賬戶上,Tom的money=money+5000元,但是該事務並未提交,而Tom正好去查看賬戶,發現工資已經到賬,賬戶money=5000元,非常高興,可是不幸的是,公司發現發給Tom的工資金額不對,應該是2000元,於是迅速回滾了事務,修改金額后,將事務提交,Tom再次查看賬戶時發現賬戶money=2000元,Tom空歡喜一場,從此郁郁寡歡,走上了不歸路……
當我們設置事務隔離級別為REPEATABLE-READ(重復讀)時事務流程如下:
事務A(代表公司) | 事務B(代表Tom) |
---|---|
read(money); | |
money=money+5000; | |
write(money) | |
read(money); | |
… | |
rollback;(money=0) | |
money=money+2000 | |
submit ; | |
read(money); |
實驗
我們在java代碼中觀察這種情況:
public class Boss {//公司給Tom發工資 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 money=money+5000 where card_id='6226090219290000'"; statement.executeUpdate(sql); Thread.sleep(10000);//10秒后發現工資發錯了 connection.rollback(); sql = "update account set money=money+2000 where card_id='6226090219290000'"; statement.executeUpdate(sql); connection.commit(); } catch (Exception e) { e.printStackTrace(); } finally { //釋放資源 } } } public class Employee {//Tom查詢余額 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方法后立即執行Employee中的main方法得:
在執行Boss中main方法后等待10秒,執行Employee中的main方法得:
得出結論
事務隔離級別為REPEATABLE-READ(重復讀)時不會出現“臟讀”
2、不可重復讀
提出問題
場景:Tom拿着工資卡去消費時,一旦POS機讀取工資卡信息(即事務開始),Tom老婆進行了轉賬並提交了事務,待Tom輸入密碼並點擊“確認”按鈕后,POS機檢查到Tom工資卡上余額沒有變化,最終扣款成功。
當我們設置事務隔離級別為REPEATABLE-READ(重復讀)時事務流程如下:
事務A(代表POS機) | 事務B(代表老婆) |
---|---|
read(money); | |
輸入密碼 | read(money); |
… | money=money-3000;(轉賬) |
… | write(money);submit ; |
read(money); | |
扣款成功! |
分析:數據庫事務隔離級別為REPEATABLE-READ(重復讀)的情況下,POS機讀取工資卡信息(此時Tom工資卡余額3000元),Tom老婆進行了轉賬並提交了事務(此時Tom工資卡余額0元),Tom輸入密碼並點擊“確認”按鈕,POS機再次讀取工資卡信息發現余額確實沒有變化,但要最后一次讀取的數據並不是來自於數據庫物理磁盤——可重復讀的隔離級別下使用了MVCC機制()“,select操作不會更新版本號,是快照讀(歷史版本);insert、update和delete會更新版本號,是當前讀(當前版本)”;
實驗
我們在java代碼中觀察這種情況:
public class Machine {//POS機扣款 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 money from account where card_id='6226090219290000'"; resultSet = statement.executeQuery(sql); if(resultSet.next()) { System.out.println("余額:"+resultSet.getDouble("money")); } System.out.println("請輸入支付密碼:"); Thread.sleep(10000);//10秒后密碼輸入成功 resultSet = statement.executeQuery(sql); if(resultSet.next()) { double money = resultSet.getDouble("money"); System.out.println("余額:"+money); if(money<sum) { System.out.println("余額不足,扣款失敗!"); return; } } sql = "update account set money=money-"+sum+" where card_id='6226090219290000'"; statement.executeUpdate(sql); connection.commit(); System.out.println("扣款成功!"); } catch (Exception e) { e.printStackTrace(); } finally { //釋放資源 } } } public class Wife {//Tom的老婆網上轉賬 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 money=money-"+money+" where card_id='6226090219290000'"; statement.executeUpdate(sql); sql = "update account set money=money+"+money+" where card_id='6226090219299999'"; statement.executeUpdate(sql); connection.commit(); System.out.println("轉賬成功"); } catch (Exception e) { e.printStackTrace(); } finally { //釋放資源 } } }
在執行Machine中main方法后立即執行Wife中的main方法得:
等待10秒后Machine中main方法執行結果:
得出結論
事務隔離級別為REPEATABLE-READ(重復讀)時不會出現“不可重復讀”。
3、幻讀
幻讀(Phantom Read): 已知有兩個事務A和B,A從一個表中讀取了數據,然后B在該表中插入了一些新數據,導致A再次讀取同一個表, 就會多出幾行。
提出問題
場景:Tom的老婆工作在銀行部門,她時常通過銀行內部系統查看Tom的工資卡消費記錄。2019年5月的某一天,她查詢到Tom當月工資卡的總消費額為80元,Tom的老婆非常吃驚,心想“老公真是太節儉了,嫁給他真好!”,而Tom此時正好在外面胡吃海塞后在收銀台買單,消費1000元,即新增了一條1000元的消費記錄並提交了事務,沉浸在幸福中的老婆查詢了Tom當月工資卡消費明細一探究竟,可查出的結果竟然發現有一筆1000元的消費,Tom的老婆瞬間怒氣沖天,外賣訂購了一個大號的榴蓮,傍晚降臨,Tom生活在了水深火熱之中,只感到膝蓋針扎的痛…
當我們設置事務隔離級別為REPEATABLE-READ(重復讀)時事務流程如下:
事務A(代表老婆) | 事務B(代表Tom消費) |
---|---|
read(消費記錄); | |
消費金額80元 | read(money); |
… | money=money-1000;(消費) |
… | write(money);submit ; |
read(消費記錄); | |
消費金額1080元 |
分析:上述情況即為幻讀,兩個並發的事務,“事務A:獲取事務B消費記錄”、“事務B:添加了新的消費記錄”,事務A獲取事務B消費記錄時數據多出了一條。
實驗
我們在java代碼中觀察這種情況:
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(10000);//10秒后查詢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 {//Tom消費1000元 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 money=money-"+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方法后立即執行Wife中的Husband方法得:
等待10秒后控制台輸出:
得出結論
事務隔離級別為REPEATABLE-READ(重復讀)時會出現“幻讀”
所用表
create table account( id int(36) primary key comment '主鍵', card_id varchar(16) unique comment '卡號', name varchar(8) not null comment '姓名', money float(10,2) default 0 comment '余額' )engine=innodb; insert into account (id,card_id,name,money) 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');