事务的ACID属性、解决脏读、不可重复读、幻读

事务的ACID属性

  1. 原子性(Atomictiy)原子性是指事务是一个不可分割的单位,事务中的操作要么都发生,要么都不发生。

    简单的来说就是在事务操作中,比如我通过两条SQL 改两条数据,要么这两个操作都完成,要么都不完成就回滚。

  2. 一致性(Consistency)事务必须从一个一致性状态变换到另一个一致性状态。

    比如转账操作,从A转给B 100元钱,那么A 少100,B 收到100,即这个转账操作就是从未转账状态到转账成功状态,类似于事务一致性的体现。

  3. 隔离性(Isolation)事务的隔离性,是指一个事务的执行不被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

    类似于多线程中的线程资源竞争问题。

  4. 持久性(Durability)一个事务一旦被提交,他对数据库中数据的改变是永久性的,接下来的其他操作对数据库故障不应该对其有任何影响。

    事务操作完数据,这个数据就被持久化了,无论数据库断电、还是故障、等都不应该再改变操作完的数据,体现其持久性。

事务并发引起的问题

事务并发一般会引起如下三个问题,分别是 脏读不可重复读幻读,这三个里面,脏读是我们必须要解决的。

1.脏读

  • 对于两个事务,T1 和 T2, T1 读取了已被T2 更新,但是还没有被提交的字段,之后,若T2 回滚,T1 读取的数据就是临时的且无效的数据。

    • 比方说,改一条数据,事务T1 用于操作改数据,T2 用于读取该后的数据,当T1 在改完还未提交事务时,T2 这个时候去读取T1 的数据,居然读出来时改之后的,这显然是有问题的,因为T1 有可能操作失败回滚,或者说T1 还未改完数据,这时T2 去读未改完的数据显然是不合理的。

2.不可重复读

  • 对于两个事务,T1,T2, T1 读取了一个字段,然后T2 更新了该字段,之后T1 再去读取同一个字段,值就不一样了。

    • 例如,我在买东西的时候,看到库存不足了,这个时候我没有关浏览器,再次刷新了浏览器,结构看到有库存了,这时因为后台数据库增加了库存,而我又没有断开此次事务连接,这样就读取到了最新的数据。(这种情况就是不可重复读问题

    • 可重复读,意思是说我一个数据库事务连接 没有断开,读取的就是我这个事务之前读取的数据,数据更新数据(事务提交),我也不应该读取事务提交后的数据,因为我这都是在一个事务中的。就说不管更新数据的事务有没有提交,只要我当前查询的事务没有关闭,就会读取之前我查询出来的状态,不管另一个更新的事务操作。

  • 我们为了解决不可重复读问题,需要让他可重复读,然而实际上大多数情况 不可重复读问题是 不需要解决的

什么又叫可重复读呢?

  • 意思就算另一个事务,改了数据(即使他commit了),如果 当前 读取数据的事务操作没有关闭,则读取的数据就还是之前的数据,如图:

  • 这种模式下,想要读取提交后的数据,必须重新起一个事务。

事务的ACID属性、解决脏读、不可重复读、幻读

3.幻读

  • 对于两个事务 T1,T2 T1从一张表中读取了一个字段后,然后T2在表中 插入了一些新的行,之后,如果T1 再次读取同一个表,就会多出几行。

    • 这里它强调的是插入操作,例如我一个事务T1读数据读出了100条,另一事务T2此时正在向此表插入数据,这时我第一个事务T1读出来就比100条多了,这种情况就是幻读

总结

注意: 不可重复读,指的是 更新 操作,而幻读,指的是 插入 操作。

一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务的隔离级别,不同隔离级别应对不同的干扰程度。

隔离级别越高,数据一致性就越好,但并发性就越弱,类似于多线程加锁,隔离性好但性能差。

一般情况下,不可重复读问题是可以接受的,一般我们只需要解决脏读的问题。

四种隔离级别

隔离级别 描述
READ UNCOMMITED 读未提交数据 允许事务读取未被其他事务提交的变更,脏读,不可重复读和幻读问题都会出现
READ COMMITED读已提交数据 只允许事务读取已被其他事务提交的变更,可以避免脏读,但不可重复读和幻读问题依然存在
REPEATABLE READ 可重复读 确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读问题,但幻读问题依然存在。
SERIALLZABLE 串行化 确保事务可以从一个表中读取相同约行,在这个事务持续期间,禁止其他事务对表执行插入更新删除操作,所有问题都可避免,但性能十分低下
  • Oracle 支持2种事务隔离级别,READ COMMITED, SERIALIZABLE. 它默认隔离级别为 READ COMMITED.

  • 隔离级别越往下,则 一致性越好,并发性越差

  • 一般情况下,脏读问题必须解决,不可重复读和幻读问题是可以接受的。

Java 隔离级别演示

如下代码

一段查询代码,根据设置的隔离级别查询数据,隔离级别如下:

TRANSACTION_REPEATABLE_READ 为可重复读,避免了脏读和可不可重复读问题.
TRANSACTION_READ_UNCOMMITTED 读未提交数据,这种隔离级别存在脏读,会读取未提交的数据
TRANSACTION_READ_COMMITTED 读已提交的数据,如果修改的数据未提交,则读出来还是老的修改之前的数据
TRANSACTION_SERIALIZABLE 串行化,解决了脏读,不可重复读,幻读。 一般不用,因为并发效率低。

查询更新的数据

一般我们都会将隔离级别设置为 TRANSACTION_READ_COMMITTED, 这样更新的事务如果没有提交,则读取的都是之前的值,这里只会读取已提交的数据。

@Test
public void testTransactionSelect() {

    Connection conn = JDBCUtils.GetDBConnection();
    try {

        //获取取数据库的隔离级别,当前为TRANSACTION_REPEATABLE_READ
        //TRANSACTION_REPEATABLE_READ 为可重复读,避免了脏读和可不可重复读问题.
        //TRANSACTION_READ_UNCOMMITTED 读未提交数据,这种隔离级别存在脏读,会读取未提交的数据
        //TRANSACTION_READ_COMMITTED 读已提交的数据,如果修改的数据未提交,则读出来还是老的修改之前的数据
        conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
        System.out.println(conn.getTransactionIsolation());

        //取消自动提交数据
        conn.setAutoCommit(false);
        String sql = "SELECT `user`,`password`,balance FROM balance WHERE `user` = ?";

        DBCommand dbCommand = new DBCommand(conn);
        BalanceEntity balance1 = dbCommand.queryMethod(BalanceEntity.class, sql, "jerry");
        System.out.println(balance1);

    } catch (Exception ex) {
        ex.printStackTrace();
    }finally {
        try {
            conn.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

更新数据操作

如下更新操作中,将数据库中的balance改为6000,中途等待15秒后提交,那么在这15秒内,查询数据库中的数据依然是之前的值。

@Test
public void testTransactionUpdate() {
    Connection conn = JDBCUtils.GetDBConnection();
    DBCommand dbCommand = new DBCommand(conn);

    try {
        String sql = "UPDATE balance SET `balance` = ? WHERE  `user`= ?";
        conn.setAutoCommit(false);

        int updateResult = dbCommand.updateMethod(conn, sql, 6000, "jerry");
        if (updateResult > 0) {
            System.out.println("更新数据成功!");
        }

        Thread.sleep(15000);

        conn.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
    }finally {
        try {
            conn.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

如之前的值为 2000 ,隔离级别设置为 TRANSACTION_READ_COMMITTED,那么在这15秒未提交的时间里,数据库查询结果和代码结果都是2000

数据库中查询结果

事务的ACID属性、解决脏读、不可重复读、幻读

代码中查询结果

事务的ACID属性、解决脏读、不可重复读、幻读

15秒后,数据提交,则查询结果如下:

事务的ACID属性、解决脏读、不可重复读、幻读