事务

事务

1.事务概述

  • 什么是事务

    Transaction 其实指的是一组操作,里面包含许多个单一的逻辑。只要有一个逻辑没有执行成功,那么都算失败。 所有的数据都回归到最初的状态(回滚)。

  • 为社么要有事务

    为了确保逻辑的成功。如银行的转账。

  • 事务的特性

    • 原子性

      指事务中包含的逻辑不可分割。

    • 一致性

      指事务执行前后数据的完整性。

    • 隔离性

      指事务在执行期间不应该受到其它事务的影响。

    • 持久性

      指事务执行成功后,数据应持久保存到磁盘上。

2.演示事务

2.1使用命令行方式演示事务

  • 开启事务

    start transaction;

  • 提交事务

    commit;提交事务后,数据将会写到磁盘上的数据库

  • 回滚事务

    rollback;数据回滚,回到最初的状态

  1. 关闭自动提交功能

  1. 开启事务

  2. 演示事务

    原来zhangsan和lisi账户各有1000块,现修改lisi的账户为1100,在事务没有提交的情况下,查出lisi账户为1100,但此时数据并没有写入到磁盘,可使用rollback回滚。

当提交事务后,再查询发现lisi账户确为1100,且无法回滚到1000。

2.2使用代码方式演示事务

模拟转账:

  • 不使用事务的情况下:
@Test
public void testTransaction() throws Exception {
    Connection con=null;
    PreparedStatement ps=null;
    ResultSet rs=null;
    con=JDBCUtils.getConnection();

    String sql="update account set money=money-? where id=?";
    ps=con.prepareStatement(sql);

    //扣ID为1的100元
    ps.setDouble(1, 100);
    ps.setInt(2, 1);
    ps.executeUpdate();

    //模拟出错
    int a=10/0;

    //加ID为2的100元
    ps.setDouble(1, -100);
    ps.setInt(2, 2);
    ps.executeUpdate();
}

转账出错后,1的钱少了100,2的钱却没有多:

  • 使用事务的情况下:
@Test
public void testTransaction2() throws Exception {
    Connection con=null;
    PreparedStatement ps=null;
    ResultSet rs=null;
    con=JDBCUtils.getConnection();
    try {
        con=JDBCUtils.getConnection();
        //关闭自动提交。事务默认是自动提交的
        con.setAutoCommit(false);
        String sql="update account set money=money-? where id=?";
        ps=con.prepareStatement(sql);

        //扣ID为1的100元
        ps.setDouble(1, 100);
        ps.setInt(2, 1);
        ps.executeUpdate();

        //模拟出错
        int a=10/0;

        //加ID为2的100元
        ps.setDouble(1, -100);
        ps.setInt(2, 2);
        ps.executeUpdate();

        //成功,提交事务
        con.commit();
    }catch(SQLException e) {
        //失败,回滚事务
        con.rollback();
    }
}

转账出错后,1的钱没有少,2的钱也没有多:

3.事务的安全隐患

3.1读

  • 脏读

    一个事务读到另外一个事务还未提交的数据。

  • 幻读

    一个事务读到了另一个事务已提交的插入的或删除的数据,导致多次查询结果不一致。

    幻读是当事务不是独立执行时发生的一种现象。

  • 不可重复读

    一个事务读到了另一个事务提交的数据, 导致多次查询结果不一致。

3.2写

  • 丢失更新

    指一个事务去修改数据库, 另一个事务也修改数据库,最后的那个事务,不管是提交还是回滚都会造成前面一个事务的数据更新丢失。

4.隔离级别

  • 读未提交(Read Uncommitted)

    一个事务可以读取到另一个事务还未提交的数据。 这就会引发 “脏读” 。读取到的是数据库内存中的数据,而并非真正磁盘上的数据。

    演示:

    1. 开启A窗口,设置隔离级别为读未提交

    1. 开启B窗口,并分别开启两个连接的事务

    1. 在B窗口执行sql语句但不提交,在A窗口重新执行查询,看到B窗口没有提交的数据

  • 读已提交(Read Committed)

    与前面的读未提交刚好相反,这个隔离级别是 ,只能读取到其他事务已经提交的数据,那些没有提交的数据读不出来。但这会造成一个问题: 前后读取到的结果不一样。 发生了不可重复读, 所谓不可重复读,就是不能执行多次读取,否则出现结果不一 。

    演示:

    1. 开启A窗口,设置A窗口的事务隔离级别为读未提交,命令如下:

      set session transaction isolation level read committed;

    2. 开启B窗口,在两个窗口中选择数据库并开启事务,在A窗口执行查询;

    3. 在B窗口执行sql语句, 但是不提交,在A窗口重新执行查询, 是不会看到B窗口刚才执行sql 语句的结果,因为它还没有提交。在B窗口执行提交,在A窗口中执行查看 这时候才会看到B窗口已经修改的结果。

  • 重复读(Repeatable Read)

    MySQL默认隔离级别。

    可以让事务在自己的会话中重复读取数据,并且不会出现结果不一样的情况,即使其他事务已经提交了,也依然还是显示以前的数据。

    演示:

    1. 开启A窗口,设置当前窗口的事务隔离级别为读未提交,命令如下:

      set session transaction isolation level repeatable read;

    2. 开启B窗口, 在两个窗口中选择数据库并开启事务,在A窗口执行查询;

    3. 在B窗口执行sql语句,但是不提交,在A窗口重新执行查询,结果不变;在B窗口提交事务后,再在A窗口重新查询,发现结果也不变,多次执行查询依然不变。

  • 可串行化(Serializable)

    最高级的事务级别,比前面几种都要强大一点,可以解决脏读、不可重复读、幻读。缺点是造成并发的性能问题。 其他的事务必须得等当前正在操作表的事务先提交,才能接着往下,否则只能一直在等着。

    演示:

    1. 开启A窗口,设置当前窗口的事务隔离级别为可串行化,命令如下:

      set session transaction isolation level serializable;

    2. 开启B窗口, 在两个窗口中选择数据库并开启事务;

      开启事务的顺序不同,会导致效果也不同:

      • A先开事务,B再开事务

        在A中执行查询,B中执行修改操作,B会卡住,因为A先开启事务,B要等A提交事务后才能执行成功。

    • B先开事务,A再开事务

      在B中执行修改操作,A中执行查询,A会卡住,A要等B提交事务后才能执行成功。

5.四种隔离级别的比较

  • 效率

    读未提交>读已提交>可重复读>可串行化

  • 拦截程度

    可串行化>可重复读>读已提交>读未提交

  • 比较

    读未提交,引发脏读。

    读已提交,引发不可重复读,解决脏读。

    可重复读,解决脏读,不可重复读,未解决幻读。

    可串行化,解决脏读,幻读,不可重复读。

5.写问题解决方法

  • 悲观锁

    指事务在一开始就认为丢失更新一定会发生, 这是一件很悲观的事情。

    具体操作步骤如下:

    1. 所有事务在执行操作前,先查询一次数据, 查询语句如下:

      select * from student for update ;

      for update 其实是数据库锁机制 、 一种排他锁;

    2. 哪个事务先执行这个语句, 哪个事务就持有了这把锁, 可以查询出来数据, 后面的事务再执行这条语句,不会有任何数据显示,就只能等着。

    3. 一直等到前面的那个事务提交数据后, 后面的事务数据才会出来,那么才可以往下接着操作。

  • 乐观锁

    指从来不会觉得丢失更新会发生。

    要求程序员在数据库中添加字段,然后在后续更新的时候,对该字段进行判定比对, 如果一致才允许更新。乐观锁的机制 ,其实是通过比对版本或者比对字段的方式来实现的。

    具体操作步骤如下:

    1. 数据库表中,额外添加了一个version字段, 用于记录版本, 默认从0 开始, 只要有针对表中数据进行修改的,那么version就+1;
    2. 开启A事务, 然后开启B事务 ;
    3. A 先执行数据库表操作。 因为以前都没有人修改过。 所以是允许A事务修改数据库的,但是修改完毕,就把version的值变成 1 了;
    4. B事务, 这时候如果想执行修改,那么是不允许修改的。 因为B事务以前是没有查询过数据库内容的,所以它认为数据库版本还是0 ,但是数据库的版本经过A修改,已经是1了。
      所以这时候不允许修改, 要求其重新查询 ;
    5. B重新查询后, 将会得到version 为 1的数据,这份数据就是之前A 事务修改的数据了, B 再进行修改,也是在A的基础上修改的,所以就不会有丢失更新的情况出现了。
原文地址:https://www.cnblogs.com/ALiWang/p/13726568.html