事务隔离级别——REPEATABLE-READ(重复读)
首先,我们先设置MySQL事务隔离级别为REPEATABLE-READ
- 在my.ini配置文件最后加上如下配置
#可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = REPEATABLE-READ
- 重启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空欢喜一场,从此郁郁寡欢,走上了不归路……
当我们设置事务隔离级别为REPEATABLE-READ(重复读)时事务流程如下:
事务A(代表公司) | 事务B(代表Tom) |
---|---|
read(money); | |
money=money+5000; | |
write(money) | |
read(money); | |
… | |
rollback;(money=0) | |
money=money+2000 | |
submit ; | |
read(money); |
实验
我们在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方法得:
得出结论
事务隔离级别为REPEATABLE-READ(重复读)时不会出现“脏读”
2、不可重复读
提出问题
场景:Tom拿着工资卡去消费时,一旦POS机读取工资卡信息(即事务开始),Tom老婆进行了转账并提交了事务,待Tom输入密码并点击“确认”按钮后,POS机检查到Tom工资卡上余额没有变化,最终扣款成功。
当我们设置事务隔离级别为REPEATABLE-READ(重复读)时事务流程如下:
事务A(代表POS机) | 事务B(代表老婆) |
---|---|
read(money); | |
输入密码 | read(money); |
… | money=money-3000;(转账) |
… | write(money);submit ; |
read(money); | |
扣款成功! |
分析:数据库事务隔离级别为REPEATABLE-READ(重复读)的情况下,POS机读取工资卡信息(此时Tom工资卡余额3000元),Tom老婆进行了转账并提交了事务(此时Tom工资卡余额0元),Tom输入密码并点击“确认”按钮,POS机再次读取工资卡信息发现余额确实没有变化,但要最后一次读取的数据并不是来自于数据库物理磁盘——可重复读的隔离级别下使用了MVCC机制()“,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)”;
实验
我们在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方法得:
等待10秒后Machine中main方法执行结果:
得出结论
事务隔离级别为REPEATABLE-READ(重复读)时不会出现“不可重复读”。
3、幻读
幻读(Phantom Read): 已知有两个事务A和B,A从一个表中读取了数据,然后B在该表中插入了一些新数据,导致A再次读取同一个表, 就会多出几行。
提出问题
场景:Tom的老婆工作在银行部门,她时常通过银行内部系统查看Tom的工资卡消费记录。2019年5月的某一天,她查询到Tom当月工资卡的总消费额为80元,Tom的老婆非常吃惊,心想“老公真是太节俭了,嫁给他真好!”,而Tom此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录并提交了事务,沉浸在幸福中的老婆查询了Tom当月工资卡消费明细一探究竟,可查出的结果竟然发现有一笔1000元的消费,Tom的老婆瞬间怒气冲天,外卖订购了一个大号的榴莲,傍晚降临,Tom生活在了水深火热之中,只感到膝盖针扎的痛…
当我们设置事务隔离级别为REPEATABLE-READ(重复读)时事务流程如下:
事务A(代表老婆) | 事务B(代表Tom消费) |
---|---|
read(消费记录); | |
消费金额80元 | read(money); |
… | money=money-1000;(消费) |
… | write(money);submit ; |
read(消费记录); | |
消费金额1080元 |
分析:上述情况即为幻读,两个并发的事务,“事务A:获取事务B消费记录”、“事务B:添加了新的消费记录”,事务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);//10秒后查询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方法得:
等待10秒后控制台输出:
得出结论
事务隔离级别为REPEATABLE-READ(重复读)时会出现“幻读”
所用表
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');