天天看點

MySQL事務隔離級别----Repeatable read(重複讀)

根據實際需求,通過設定資料庫的事務隔離級别可以解決多個事務并發情況下出現的髒讀、不可重複讀和幻讀問題,資料庫事務隔離級别由低到高依次為Read uncommitted、Read committed、Repeatable read和Serializable等四種。資料庫不同,其支援的事務隔離級别亦不相同:MySQL資料庫支援上面四種事務隔離級别,預設為Repeatable read;Oracle 資料庫支援Read committed和Serializable兩種事務隔離級别,預設為Read committed。  

Repeatable read(重複讀):可以避免髒讀和不可重複讀,但可能出現幻讀。

注意:事務隔離級别為可重複讀時,如果檢索條件有索引(包括主鍵索引)的時候,預設加鎖方式是next-key 鎖;如果檢索條件沒有索引,更新資料時會鎖住整張表。一個間隙被事務加了鎖,其他事務是不能在這個間隙插入記錄的,這樣可以防止幻讀。

MySQL資料庫,預設為Repeatable read(重複讀)

資料庫事務隔離級别設定為REPEATABLE-READ(重複讀):在并重新開機MySQL服務。  

MySQL事務隔離級别----Repeatable read(重複讀)
MySQL事務隔離級别----Repeatable read(重複讀)

髒讀 

已知有兩個事務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);
           
 資料庫中資料顯示: 
MySQL事務隔離級别----Repeatable read(重複讀)
代碼如下: 
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方法輸出

MySQL事務隔離級别----Repeatable read(重複讀)
MySQL事務隔離級别----Repeatable read(重複讀)

先執行Boss類中main方法——>再執行Employye類中main方法----運作結果:1000——>Boss類中main方法執行完畢——>再執行Employye類中main方法——>觀察Employye類中main方法輸出----> 30秒線程阻塞後,運作結果為3000 

 兩個并發的事務:“事務B:上司給Tom發工資”、“事務A:Tom查詢工資賬戶”,在Read committed(讀已送出)級别下,事務A沒有讀取了事務B尚未送出的資料。 此時并沒有出現髒讀的情況,是以當事務的隔離級别為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再次讀取該資料扣款

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);
           
資料庫中結果顯示:
MySQL事務隔離級别----Repeatable read(重複讀)
代碼如下:
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方法輸出: 

MySQL事務隔離級别----Repeatable read(重複讀)
MySQL事務隔離級别----Repeatable read(重複讀)
MySQL事務隔離級别----Repeatable read(重複讀)
兩個并發的事務,“事務A:POS機扣款”、“事務B:Tom的老婆網上轉賬”,事務A事先讀取了資料,事務B緊接了更新資料并送出了事務,而事務A再次讀取該資料扣款時,資料沒有發生了改變。 上面的結果顯示并沒有出現不可重複讀,所有當事務的隔離級别設定為REPEATABLE-READ的時候可以避免不可重複讀,妻子轉賬成功了,Tom也成功的支付了費用,但是卡中隻有3000元,檢視資料庫中的資料:
MySQL事務隔離級别----Repeatable read(重複讀)
Tom賬戶中的金額變成了負數這是因為:在資料庫事務隔離級别為REPEATABLE-READ(重複讀)的情況下,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:添加了新的消費記錄”,

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資料庫表中的資料:
MySQL事務隔離級别----Repeatable read(重複讀)
record資料庫表中的資料:
MySQL事務隔離級别----Repeatable read(重複讀)
代碼如下: 
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方法輸出:

MySQL事務隔離級别----Repeatable read(重複讀)
30秒線程阻塞後列印的消費明細:  
MySQL事務隔離級别----Repeatable read(重複讀)
兩個并發的事務,“事務A:擷取事務B消費記錄”、“事務B:添加了新的消費記錄”,事務A擷取事務B消費記錄時資料,而此時并不是避免了幻讀,由mvcc機制可知,最後一次資料---列印出的消費明細是來自在緩存上的資料而不是磁盤上的,此時的資料庫中消費記錄多出了一條:
MySQL事務隔離級别----Repeatable read(重複讀)