事务隔离级别—— SERIALIZABLE(序列化)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Zzze0101/article/details/91345315

首先,我们先设置MySQL事务隔离级别为SERIALIZABLE

  1. 在my.ini配置文件最后加上如下配置
#可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = SERIALIZABLE
 
  1. 重启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空欢喜一场,从此郁郁寡欢,走上了不归路……
当我们设置事务隔离级别为SERIALIZABLE(序列化)时事务流程如下:

事务A(代表公司)事务B(代表Tom)
read(money);  
money=money+5000;  
write(money)  
  read(money);(操作未成功!)
 
rollback;(money=0)  
money=money+2000  
submit ;  
  read(money);(操作成功)

分析:上述情况即为脏读,两个并发的事务:“事务A:公司给Tom发工资”、“事务B:Tom查询工资账户”,事务隔离级别为SERIALIZABLE(序列化)时事务B只能在事务A提交后执行。
实验
我们在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方法得:
在这里插入图片描述
得出结论
事务隔离级别为SERIALIZABLE(序列化)时不会出现“脏读”

2、不可重复读

提出问题
场景:Tom拿着工资卡去消费,酒足饭饱后在收银台买单,服务员告诉他本次消费1000元,Tom将银行卡给服务员,服务员将银行卡插入POS机,POS机读到卡里余额为3000元,就在Tom磨磨蹭蹭输入密码时,他老婆以迅雷不及掩耳盗铃之势把Tom工资卡的3000元转到自己账户并提交了事务,当Tom输完密码并点击“确认”按钮后,POS机检查到Tom的工资卡已经没有钱,扣款失败,Tom十分纳闷,明明卡里有钱,于是怀疑POS有鬼,和收银小姐姐大打出手,300回合之后终因伤势过重而与世长辞,Tom老婆痛不欲生,郁郁寡欢,从此走上了不归路…
当我们设置事务隔离级别为SERIALIZABLE(序列化)时事务流程如下:

事务A(代表POS机)事务B(代表老婆)
read(money);  
输入密码 read(money);(操作未成功!等待)
read(money);  
submit ;消费成功!  
read(money);(money变为2000)
money=money-2000;(转账)
write(money);submit ;

分析:上述情况即为不可重复读,两个并发的事务,“事务A:POS机扣款”、“事务B:Tom的老婆网上转账”,事务A事先读取了数据,事务B也要读取数据,但是在事务隔离级别为SERIALIZABLE(序列化)的情况下,读取失败,事务A提交后事务B才可进行。
实验
我们在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方法得:

com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:406)
    at com.mysql.jdbc.Util.getInstance(Util.java:381)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1045)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3558)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3490)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1959)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2109)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2637)
    at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1647)
    at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1566)
    at Wife.main(Wife.java:16)

 

控制台报错

等待10秒后Machine中main方法在控制台中没有执行结果,我们直接打开数据库表:
在这里插入图片描述
转账操作未成功!
得出结论
事务隔离级别为SERIALIZABLE(序列化)时不允许其他事务与正在执行事务并发执行,不会出现“不可重复读”。

3、幻读

幻读(Phantom Read): 已知有两个事务A和B,A从一个表中读取了数据,然后B在该表中插入了一些新数据,导致A再次读取同一个表, 就会多出几行。
提出问题
场景:Tom的老婆工作在银行部门,她时常通过银行内部系统查看Tom的工资卡消费记录。2019年5月的某一天,她查询到Tom当月工资卡的总消费额为80元,Tom的老婆非常吃惊,心想“老公真是太节俭了,嫁给他真好!”,而Tom此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录并提交了事务,沉浸在幸福中的老婆查询了Tom当月工资卡消费明细一探究竟,可查出的结果竟然发现有一笔1000元的消费,Tom的老婆瞬间怒气冲天,外卖订购了一个大号的榴莲,傍晚降临,Tom生活在了水深火热之中,只感到膝盖针扎的痛…
当我们设置事务隔离级别为SERIALIZABLE(序列化)时事务流程如下:

事务A(代表老婆)事务B(代表Tom消费)
read(消费记录);  
消费金额80元 read(money);(操作未成功!等待)
read(消费记录);submit;  
消费金额80元 read(money);(操作成功!)
money=money-1000;(消费)
write(money);submit ;

分析:上述情况并没有出现场景中的幻读,在事务隔离级别为SERIALIZABLE(序列化)的情况下,事务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);//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 {//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方法得:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '3' for key 'PRIMARY'
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at com.mysql.jdbc.Util.handleNewInstance(Util.java:406)
    at com.mysql.jdbc.Util.getInstance(Util.java:381)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1015)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3558)
    at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3490)
    at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1959)
    at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2109)
    at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2637)
    at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1647)
    at com.mysql.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1566)
    at Husband.main(Husband.java:18)

 

控制台报错
等待10秒后控制台输出:在这里插入图片描述
得出结论
事务隔离级别为SERIALIZABLE(序列化)时不允许其他事务与正在执行事务并发执行,不会出现“幻读”

所用表

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

 

posted @ 2019-11-18 16:11  门罗的魔术师  阅读(2510)  评论(0编辑  收藏  举报