同一个应用程序中的多个事务或不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题,这些问题可分为如下三种类型:
第一种:脏读(Dirty Read),已有两个事务A、B,事务A读取了事务B更新了但是没有提交的数据,之后B 回滚事务,A读到的数据就是脏数据。
第二种:不可重复读(Non-repeated Read),有两个事务A、B,事务A多次读取同一数据,B在A读取时将事务进行了修改并提交,导致A多次读取同一数据的结果不一致。
第三种:幻读(Phontam Read),已知有两个事务A和B,A从一个表中读取了数据,然后B在该表中插入一些新数据,导致A再次读取同一个表, 就会多出几行,简单地说,一个事务中先后读取一个范围的记录,但每次读取的纪录数不同,称之为幻象读。
幻读和不可重复读的主要差别就是:在多次的查询中,数据的修改导致数据内容前后不一致为不可重复读错误;数据的添加和删除导致数据前后数量不一致为幻读。
为解决这些并发执行出现的问题,可以根据实际情况通过设置数据库的事务隔离级别可以解决多个事务并发情况下出现的脏读、不可重复读、幻读数问题。数据库事务隔离级别由低到高依次为Read uncommitted、Read committed、Repeatable read和Serializable等四种。数据库不同,其支持的事务隔离级别亦不相同:MySQL数据库支持上面四种事务隔离级别,默认的数据库隔离级别为Repeatable read。
事务隔离级别:
一、Read uncommitted(读未提交):可能会出现脏读、不可重复读、幻读。
二、Read committed(读已提交):可能会出现不可重复读、幻读,能够避免脏读。
三、Repeatable read(可重复读):可以避免脏读和不可重复读,但是可能会出现幻读。
四、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; |
1.READ-UNCOMMITTED(读未提交)
首先将数据库事务隔离级别设置为READ-UNCOMMITTED(读未提交)并重启MySQL服务。
场景:公司发工资了,领导把5000元打到Tom的账号上,但是该事务并未提交,而Tom正好去查看账户,发现工资已经到账,账户多了5000元,非常高兴,可是不幸的是,领导发现发给Tom的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,Tom再次查看账户时发现账户只多了2000元
分析:上述情况即为脏读,两个并发的事务:“事务B:领导给Tom发工资”、“事务A:Tom查询工资账户”,事务A读取了事务B尚未提交的数据。
创建一个account表
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);
import java.sql.*;
public class Employee {
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 {
//释放资源
}
}
}
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 {
//释放资源
}
}
}
执行Boss类中main方法——>执行Employee类中main方法——>Boss类中main方法执行完成——>再次执行Employee中main方法,观察效果。
会发现第一次读取的为6000,再次读取时却变成了3000,此时就是脏读,6000就是产生的脏数据。
2.READ-COMMITTED(读已提交)
首先将数据库事务隔离级别设置为READ-COMMITTED(读未提交)并重启MySQL服务。
此时重复前一个脏读实例会发现,在Boss类执行事务后,Employee无论何时执行时显示为1000,只有在提交事务后,Employee才显示为3000。(中间的未提交的操作对Employee无影响)
场景:Tom拿着工资卡去消费,酒足饭饱后在收银台买单,服务员告诉他本次消费1000元,Tom将银行卡给服务员,服务员将银行卡插入POS机,POS机读到卡里余额为3000元,就在Tom磨磨蹭蹭输入密码时,他老婆正好把Tom工资卡的3000元转到自己账户并提交了事务,当Tom输完密码并点击“确认”按钮后,POS机检查到Tom的工资卡已经没有钱,扣款失败,Tom十分纳闷
分析:上述情况即为不可重复读,两个并发的事务,“事务A:POS机扣款”、“事务B:Tom的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新数据并提交了事务,而事务A再次读取该数据扣款时,数据已经发生了改变。
创建一个新表account
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);
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
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 {
//释放资源
}
}
}
先执行Machine类中main方法——>再执行Wife类中main方法——>观察Machine类中main方法输出
结果发现查询时余额为3000,在Wife执行事务后,密码输入完成时余额为0,付款失败。此时就是不可重复读错误,导致两次余额读取不一致。
3.Repeatable read(可重复读)
数据库事务隔离级别设置为REPEATABLE-READ(重复读)并重启MySQL服务。
场景:Tom拿着工资卡去消费,酒足饭饱后在收银台买单,服务员告诉他本次消费1000元,Tom将银行卡给服务员,服务员将银行卡插入POS机,POS机读到卡里余额为3000元,就在Tom磨磨蹭蹭输入密码时,他老婆以迅雷不及掩耳盗铃之势把Tom工资卡的3000元转到自己账户并提交了事务,当Tom输完密码并点击“确认”按钮后,POS机检查到Tom的工资卡已经没有钱,扣款失败,Tom十分纳闷
分析:上述情况即为不可重复读,两个并发的事务,“事务A:POS机扣款”、“事务B:Tom的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新数据并提交了事务,而事务A再次读取该数据扣款时,数据已经发生了改变。
REPEATABLE-READ下的重复读:
场景:Tom拿着工资卡去消费时,一旦POS机读取工资卡信息(即事务开始),Tom老婆进行了转账并提交了事务,待Tom输入密码并点击“确认”按钮后,POS机检查到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',3000);
insert into account (id,card_id,name,balance) values (2,'6226090219299999','LilY',0);
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
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 {
//释放资源
}
}
}
先执行Machine类中main方法——>再执行Wife类中main方法——>观察Machine类中main方法输出
结果妻子也转账成功,Tom也支付成功,但是在查看数据库中数据会发现Tom余额-1000,妻子余额3000。
注意:
思考:为什么程序执行完毕,Tom工资卡余额为-1000元
分析:数据库事务隔离级别为REPEATABLE-READ(重复读)的情况下,POS机读取工资卡信息(此时Tom工资卡余额3000元),Tom老婆进行了转账并提交了事务(此时Tom工资卡余额0元),Tom输入密码并点击“确认”按钮,POS机再次读取工资卡信息发现余额确实没有变化,但要最后一次读取的数据并不是来自于数据库物理磁盘——“可重复读的隔离级别下使用了MVCC机制select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)”;