首先,我們先設置MySQL事務隔離級別為SERIALIZABLE
- 在my.ini配置文件最后加上如下配置
#可選參數有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = SERIALIZABLE
- 重啟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空歡喜一場,從此郁郁寡歡,走上了不歸路……
當我們設置事務隔離級別為SERIALIZABLE(序列化)時事務流程如下:
事務A(代表公司) | 事務B(代表Tom) |
---|---|
read(money); | |
money=money+5000; | |
write(money) | |
read(money);(操作未成功!) | |
… | |
rollback;(money=0) | |
money=money+2000 | |
submit ; | |
read(money);(操作成功) |
分析:上述情況即為臟讀,兩個並發的事務:“事務A:公司給Tom發工資”、“事務B:Tom查詢工資賬戶”,事務隔離級別為SERIALIZABLE(序列化)時事務B只能在事務A提交后執行。
實驗
我們在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方法得:
得出結論
事務隔離級別為SERIALIZABLE(序列化)時不會出現“臟讀”
2、不可重復讀
提出問題
場景:Tom拿着工資卡去消費,酒足飯飽后在收銀台買單,服務員告訴他本次消費1000元,Tom將銀行卡給服務員,服務員將銀行卡插入POS機,POS機讀到卡里余額為3000元,就在Tom磨磨蹭蹭輸入密碼時,他老婆以迅雷不及掩耳盜鈴之勢把Tom工資卡的3000元轉到自己賬戶並提交了事務,當Tom輸完密碼並點擊“確認”按鈕后,POS機檢查到Tom的工資卡已經沒有錢,扣款失敗,Tom十分納悶,明明卡里有錢,於是懷疑POS有鬼,和收銀小姐姐大打出手,300回合之后終因傷勢過重而與世長辭,Tom老婆痛不欲生,郁郁寡歡,從此走上了不歸路…
當我們設置事務隔離級別為SERIALIZABLE(序列化)時事務流程如下:
事務A(代表POS機) | 事務B(代表老婆) |
---|---|
read(money); | |
輸入密碼 | read(money);(操作未成功!等待) |
read(money); | |
submit ;消費成功! | |
… | read(money);(money變為2000) |
… | money=money-2000;(轉賬) |
… | write(money);submit ; |
分析:上述情況即為不可重復讀,兩個並發的事務,“事務A:POS機扣款”、“事務B:Tom的老婆網上轉賬”,事務A事先讀取了數據,事務B也要讀取數據,但是在事務隔離級別為SERIALIZABLE(序列化)的情況下,讀取失敗,事務A提交后事務B才可進行。
實驗
我們在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方法得:
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) at java.lang.reflect.Constructor.newInstance(Unknown Source) at com.mysql.jdbc.Util.handleNewInstance(Util.java:406) at com.mysql.jdbc.Util.getInstance(Util.java:381) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1045) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3558) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3490) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1959) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2109) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2637) at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1647) at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1566) at Wife.main(Wife.java:16)
控制台報錯
等待10秒后Machine中main方法在控制台中沒有執行結果,我們直接打開數據庫表:
轉賬操作未成功!
得出結論
事務隔離級別為SERIALIZABLE(序列化)時不允許其他事務與正在執行事務並發執行,不會出現“不可重復讀”。
3、幻讀
幻讀(Phantom Read): 已知有兩個事務A和B,A從一個表中讀取了數據,然后B在該表中插入了一些新數據,導致A再次讀取同一個表, 就會多出幾行。
提出問題
場景:Tom的老婆工作在銀行部門,她時常通過銀行內部系統查看Tom的工資卡消費記錄。2019年5月的某一天,她查詢到Tom當月工資卡的總消費額為80元,Tom的老婆非常吃驚,心想“老公真是太節儉了,嫁給他真好!”,而Tom此時正好在外面胡吃海塞后在收銀台買單,消費1000元,即新增了一條1000元的消費記錄並提交了事務,沉浸在幸福中的老婆查詢了Tom當月工資卡消費明細一探究竟,可查出的結果竟然發現有一筆1000元的消費,Tom的老婆瞬間怒氣沖天,外賣訂購了一個大號的榴蓮,傍晚降臨,Tom生活在了水深火熱之中,只感到膝蓋針扎的痛…
當我們設置事務隔離級別為SERIALIZABLE(序列化)時事務流程如下:
事務A(代表老婆) | 事務B(代表Tom消費) |
---|---|
read(消費記錄); | |
消費金額80元 | read(money);(操作未成功!等待) |
read(消費記錄);submit; | |
消費金額80元 | read(money);(操作成功!) |
… | money=money-1000;(消費) |
… | write(money);submit ; |
分析:上述情況並沒有出現場景中的幻讀,在事務隔離級別為SERIALIZABLE(序列化)的情況下,事務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);//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 {//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方法得:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '3' for key 'PRIMARY' at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) at java.lang.reflect.Constructor.newInstance(Unknown Source) at com.mysql.jdbc.Util.handleNewInstance(Util.java:406) at com.mysql.jdbc.Util.getInstance(Util.java:381) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015) at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3558) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3490) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1959) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2109) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2637) at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1647) at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1566) at Husband.main(Husband.java:18)
控制台報錯
等待10秒后控制台輸出:
得出結論
事務隔離級別為SERIALIZABLE(序列化)時不允許其他事務與正在執行事務並發執行,不會出現“幻讀”
所用表
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');