Java之mySQL數據庫事務隔離級別

問題

同一個應用程序中的多個事務或不同應用程序中的多個事務在同一個數據集上併發執行時, 可能會出現許多意外的問題,這些問題可分爲如下三種類型:

  1. 髒讀(Drity Read): 已知有兩個事務A和B, A讀取了已經被B更新但還沒有被提交的數據,之後,B回滾事務,A讀取的數據就是髒數據。

    場景: 公司發工資了,領導把5000元打到Tom的賬號上,但是該事務並未提交,而Tom正好去查看賬戶,發現工資已經到賬,賬戶多了5000元,非常高興,可是不幸的是,領導發現發給Tom的工資金額不對,是2000元,於是迅速回滾了事務,修改金額後,將事務提交,Tom再次查看賬戶時發現賬戶只多了2000元,Tom空歡喜一場,從此鬱鬱寡歡,走上了不歸路……
    分析: 上述情況即爲髒讀,兩個併發的事務:“事務B:領導給Tom發工資”、“事務A:Tom查詢工資賬戶”,事務A讀取了事務B尚未提交的數據。

  2. 不可重複讀(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再次讀取該數據扣款時,數據已經發生了改變。

  3. 幻讀(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消費記錄時數據多出了一條。

解決方案

根據實際需求,通過設置數據庫的事務隔離級別可以解決多個事務併發情況下出現的髒讀、不可重複讀和幻讀問題,數據庫事務隔離級別由低到高依次爲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數據庫的隔離級別:

#可以通過如下語句來查看數據庫隔離級別
select @@global.tx_isolation,@@tx_isolation;

修改mySQL數據庫的隔離級別:

  1. 全局修改:
    ①、在my.ini配置文件最後加上如下配置:
    ②、重啓MySQL服務
#可選參數有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = READ-UNCOMMITTED
  1. 當前session修改,登錄MySQL數據庫後執行如下命令:
#指令有 read uncommitted,read committed,repeatable read,serializable
set session transaction isolation level read uncommitted;

Java代碼實現

1、髒讀:
先創建表和插入數據:

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);

再通過Java實現領導打錢和員工查詢:

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 {
			//釋放資源
		}
	}
}

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 {
			//釋放資源
		}
	}
}

2、不可重複讀:
先創建表和插入數據:

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);

再通過Java實現付費和轉賬:

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 {
			//釋放資源
		}
	}
}
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 {
			//釋放資源
		}
	}
}

3、幻讀:
先創建表和插入數據:

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');

再通過Java實現查詢和消費:

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 {
			//釋放資源
		}
	}
}
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 {
			//釋放資源
		}
	}
}

當使用repeatable read解決不可重複度問題時:
在Tom輸入密碼的時候,Lily仍可以進行轉賬操作並且轉賬成功,然後Tom輸入密碼完成,Pos機在進行校驗時讀取到的餘額與第一次無變化,故成功進行交易,最後,Tom最終工資卡餘額爲-1000元,Lily的餘額爲3000元。

分析:數據庫事務隔離級別爲REPEATABLE-READ(重複讀)的情況下,POS機讀取工資卡信息(此時Tom工資卡餘額3000元),Tom老婆進行了轉賬並提交了事務(此時Tom工資卡餘額變爲0元),但Tom輸入密碼並點擊“確認”按鈕,POS機再次讀取工資卡信息發現餘額確實沒有變化,因爲最後一次讀取的數據並不是來自於數據庫物理磁盤,而是來自於緩存 ——“MVCC機制”

SERIALIZABLE下的重複讀:
Tom拿着工資卡去消費時,一旦POS機讀取工資卡信息(即事務開始),Lily不能在此時轉賬,待Tom輸入密碼並點擊“確認”按鈕後,POS機檢查到Tom工資卡上餘額沒有變化,最終扣款成功,之後Lily才能進行轉賬操作。

分析:serializable隔離機制在讀寫數據時,會把整張表完全鎖死,只有在此次操作結束並提交,才能繼續執行其他操作。(這種隔離機制會使併發性降得極低,從而嚴重影響性能,共一般不適用)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章