事务隔离级别

问题引入:

同一个应用程序中的多个事务或不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题,这些问题可分为如下三种类型:

脏读(Drity Read): 已知有两个事AB, A读取了已经被B更新但还没有被提交的数据,之后B回滚事务,A读取的数据就是脏数据(即修改之前的数据)。

模拟情景:

场景:公司发工资了,领导把5000元打到Tom的账号上,但是该事务并未提交,而Tom正好去查看账户,发现工资已经到账,账户多了5000元,非常高兴,可是不幸的是,领导发现发给Tom的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,Tom再次查看账户时发现账户只多了2000元,Tom空欢喜一场。

分析:上述情况即为脏读,两个并发的事务:“事务B:领导给Tom发工资”、“事务A:Tom查询工资账户”,事务A读取了事务B尚未提交的数据。

事务B:

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 {
		//释放资源
	}
    }
}

事务A:

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 {
		//释放资源
	}
    }
}

不可重复(Non-repeatable read):已知有两个事AB,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再次读取该数据扣款时,数据已经发生了改变。

 

场景:Tom拿着工资卡去消费时,一旦POS机读取工资卡信息(即事务开始),Tom老婆即便进行了转账,待Tom输入密码并点击“确认”按钮后,POS机检查到Tom工资卡上余额没有变化,最终扣款成功。

分析:上述情况即为重复读

事务A:

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 {
		//释放资源
	}
}
}

事务B:

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 {
		//释放资源
        }
    }
}

幻读(Phantom Read): 已知有两个事ABA从一个表中读取了数据,然后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消费记录时数据多出了一条。

场景:教师A执行SQL语句将数据库中所有学生的成绩从具体分数改为ABCDE等级制,该SQL语句执行后事务提交前,教师B插入了一条具体分数的记录并提交了事务,教师A事务提交并执行查询SQL语句,此时教师A发现还有一条记录没有改过来,就好像发生了幻觉一样。

事务A:

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 {
		//释放资源
	}
    }
}

事务B:

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 {
		//释放资源
	}
    }
}

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

解决方案

根据实际需求,通过设置数据库的事务隔离级别可以解决多个事务并发情况下出现的脏读、不可重复读和幻读问题,数据库事务隔离级别由低到高依次为Read uncommitted、Read committed、Repeatable read和Serializable等四种。数据库不同,其支持的事务隔离级别亦不相同:MySQL数据库支持上面四种事务隔离级别,默认为Repeatable read;Oracle 数据库支持Read committedSerializable两种事务隔离级别,默认为Read committed

1、Read uncommitted(读未提交):可能出现脏读、不可重复读和幻读。

2、Read committed(读提交):可以避免脏读,但可能出现不可重复读和幻读。大多数数据库默认级别就是Read committed,比如Sql Server数据库和Oracle数据库。注意:该隔离级别在写数据时只会锁住相应的行。

3、Repeatable read(重复读):可以避免脏读和不可重复读,但可能出现幻读。注意:①、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;②、如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。

4、Serializable(序列化):可以避免脏读、不可重复读和幻读,但是并发性极低,一般很少使用。注意:该隔离级别在读写数据时会锁住整张表。

说明:√表示可能出现,×表示不会出现

注注意意注意:隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

MySQL事务隔离级别

查看:MySQL数据库支持Read uncommitted、Read committed、Repeatable read和Serializable四种事务隔离级别,默认为Repeatable read,可以通过如下语句查看MySQL数据库事务隔离级别:

select @@global.tx_isolation,@@tx_isolation;

修改:MySQL数据库事务隔离级别的修改分为全局修改和当前session修改,具体修改方法如下:

1、全局修改

①、在my.ini配置文件最后加上如下配置:

#可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.

[mysqld]

transaction-isolation = READ-UNCOMMITTED

、重启MySQL服务

2、当前session修改,登录MySQL数据库后执行如下命令:

set session transaction isolation level read uncommitted;

 

 

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