脏读、幻读和不可重复读

脏读、幻读和不可重复读

前言:

在多个并发事务并行执行时,如果多个事务同时读写,可能会出现脏读、幻读和不可重复读情况。

事务

数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单元,由一个有限的数据库操作序列构成。——维基百科
简而言之:一系列数据库操作语句组成事务。

脏读(dirty read)

脏读是指一个事务读取并使用了另外一个事务未提交的数据。因为一个事务可以提交或者回滚,可能会导致这个事务读错数据。在多个并发事务并行执行时,如果一个事务修改了数据但还未提交,而另外一个事务读取了这个未提交的数据,那么它读取的数据可能是不准确或不完整的。这就是脏读。

脏读可能会导致严重的数据一致性问题,因为还未提交的数据可能会在稍后被回滚或提交,这将使得先前读取的脏数据不一致。脏读是数据并发控制中最基本的问题之一,因此数据库系统提供了各种隔离级别来处理并发事务。事务隔离可以参考另外一篇博客。

为了避免脏读,可以使用较高的隔离级别(如读已提交,可重复读或串行化),以确保事务只读取到已经提交的数据,或者使用锁机制来限制对数据的访问。通常,隔离级别越高,数据一致性就越好,但是对数据库性能的影响也会更大。

例子:

张三的工资为 5000, 事务 A 中把他的工资改为 8000, 但事务 A 尚未提交。与此同时,事务 B 正在读取张三的工资,读取到张三的工资为 8000。随后,事务 A 发生异常,而回滚了事务。张三的工资又回滚为 5000。最后,事务 B 读取到的张三工资为 8000 的数据即为脏数据,事务 B 做了一次脏读。

Java例子,

import java.sql.*;

public class DirtyReadExample {
    public static void main(String[] args) {
        try {
            // 连接数据库
            Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "username", "password");
            conn.setAutoCommit(false);

            // 开始事务A,修改张三的工资并回滚
            Statement stmtA = conn.createStatement();
            stmtA.executeUpdate("UPDATE employee SET salary = 8000 WHERE name = '张三'");
            System.out.println("事务A修改后的工资:" + getSalary(conn, "张三"));
            conn.rollback();
            System.out.println("事务A回滚后的工资:" + getSalary(conn, "张三"));

            // 开始事务B,在事务A未提交时读取张三的工资
            Statement stmtB = conn.createStatement();
            System.out.println("事务B读取到的工资:" + getSalary(conn, "张三"));

            // 关闭连接
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    // 查询指定姓名的员工工资
    private static int getSalary(Connection conn, String name) throws SQLException {
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT salary FROM employee WHERE name = '" + name + "'");
        if (rs.next()) {
            return rs.getInt("salary");
        }
        return 0;
    }
}

在这个代码中,我们使用了事务来模拟并发操作。首先,我们开启了事务A,修改张三的工资为8000,但是事务A又回滚了。接着,我们在事务A未提交时开启事务B,查询张三的工资。由于事务A尚未提交,事务B就提取到了事务A修改但未提交的数据,也就是脏数据。当事务A回滚后,张三的工资又被回滚回去,但是事务B已经读取了脏数据,因此最后输出的结果是脏数据,即8000。

不可重复读(non-repeatable read)

不可重复读(Non-repeatable Read)是指在一个事务内,两次相同的查询却返回了不同的结果,这是由于在这个事务期间,另外一个事务修改或者删除了这个数据。

例子:

假设在事务A中,我们先查询了一个用户的姓名,结果是“Tom“,但是在查询的过程中,事务B修改了这个用户的姓名为”Jerry“并提交了事务。此时,如果在事务A中再次查询这个用户的姓名,会得到”Jerry“这个不同的结果,导致了不可重复读现象。

在不可重读的情况下,每次读取的数据可能是不同的,因此数据的准确性无法得到保证,这对于一些要求数据的一致性和准确性的应用场景是不可接受的。为了避免这种情况,我们可以采用更高级别的隔离级别(如可重复读或串行化)来保证数据的一致性和准确性。

简单的Java代码示例:

import java.sql.*;

public class UnrepeatableReadExample {

    public static void main(String[] args) throws SQLException, InterruptedException {
        // 数据库连接信息
        String url = "jdbc:mysql://localhost:3306/testdb";
        String user = "root";
        String password = "password";

        // 连接数据库
        Connection conn = DriverManager.getConnection(url, user, password);

        // 设置隔离级别为 READ COMMITTED
        conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

        // 开启事务,关闭自动提交
        conn.setAutoCommit(false);

        // 在事务中执行查询
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery("SELECT name FROM users WHERE id = 1");

        // 输出查询结果
        while (rs.next()) {
            System.out.println("First read result: " + rs.getString("name"));
        }

        // 在事务中等待一段时间
        System.out.println("Waiting for 10 seconds...");
        Thread.sleep(10000);

        // 再次执行相同的查询
        ResultSet rs2 = stmt.executeQuery("SELECT name FROM users WHERE id = 1");

        // 输出查询结果
        while (rs2.next()) {
            System.out.println("Second read result: " + rs2.getString("name"));
        }

        // 提交事务
        conn.commit();

        // 关闭连接
        conn.close();
    }
}

我们需要在等待的这10秒之内将数据库中id = 1的name:Tom改为其他的name:Tomm,结果如下所示:

First read result: Tom
Waiting for 5 seconds...
Second read result: Tomm

Process finished with exit code 0

在这个例子中,我们首先将隔离级别设置为READ COMMITTED,然后开启一个事务,并执行了一个查询。在查询完成后,我们等待了一段时间(这里是10秒),然后再次执行相同的查询。由于在等待的过程中,我们手动的将数据库里面id=1 的name改为“Tomm”,所以第二次查询的结果返回了“Tomm”,导致了不可重复读的现象。

幻读(phantom read)

幻读是指在一个事务执行的过程中,前后两次查询同一范围的数据时,由于其他事务插入或删除数据,导致前后两次查询返回的结果集不同的情况。

幻读和不可重复读类似,不同之处在于不可重复读是指同一个事务中的两次查询结果不一致,而幻读则是指不同事务中的两次查询结果不一致。幻读通常发生在并发环境下,当一个事务查询了某个范围内的数据,并再次基础上做出了一些决策,但此时另一个事务又插入了一些新数据,导致前一个事务再次查询该范围内的数据时,发现范围内多了一些之前不存在的新数据,从而产生了幻读。

举个例子,假设有两个事务T1和T2,T1查询了表中年龄小于30岁的所有员工,并计算了这些员工的平均工资,然后T2插入了一条新的记录,其中的年龄小于30岁,但薪资非常高,导致T1再次查询时得到的平均工资与之前的结果不一致,从而产生了幻读。

为了避免幻读问题,可以使用更高的隔离级别,如串行化隔离级别,或者使用行锁或表锁等机制限制其他事务对数据的修改。

Java简单的代码:

import java.sql.*;

public class PhantomReadExample {
    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
    static final String DB_URL = "jdbc:mysql://localhost:3306/testdb";
    static final String USER = "username";
    static final String PASS = "password";

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        try {
            // Register JDBC driver
            Class.forName(JDBC_DRIVER);

            // Open a connection
            System.out.println("Connecting to database...");
            conn = DriverManager.getConnection(DB_URL, USER, PASS);

            // Turn off auto-commit
            conn.setAutoCommit(false);

            // Create a statement
            System.out.println("Creating statement...");
            stmt = conn.createStatement();

            // Execute the first select query
            ResultSet rs = stmt.executeQuery("SELECT * FROM employees WHERE salary > 50000");

            // Print the result
            System.out.println("First select query result:");
            while (rs.next()) {
                String name = rs.getString("name");
                int salary = rs.getInt("salary");
                System.out.println("Name: " + name + ", Salary: " + salary);
            }

            // Insert a new row
            System.out.println("Inserting a new row...");
            stmt.executeUpdate("INSERT INTO employees (name, salary) VALUES ('John', 60000)");

            // Execute the second select query
            rs = stmt.executeQuery("SELECT * FROM employees WHERE salary > 50000");

            // Print the result
            System.out.println("Second select query result:");
            while (rs.next()) {
                String name = rs.getString("name");
                int salary = rs.getInt("salary");
                System.out.println("Name: " + name + ", Salary: " + salary);
            }

            // Commit the transaction
            conn.commit();

            // Close the result set and statement
            rs.close();
            stmt.close();

            // Close the connection
            conn.close();
            System.out.println("Connection closed.");
        } catch (SQLException se) {
            // Handle errors for JDBC
            se.printStackTrace();
            // Rollback the transaction
            try {
                if (conn != null) {
                    conn.rollback();
                }
            } catch (SQLException se2) {
                se2.printStackTrace();
            }
        } catch (Exception e) {
            // Handle errors for Class.forName
            e.printStackTrace();
        } finally {
            // Close resources
            try {
                if (stmt != null) {
                    stmt.close();
                }
            } catch (SQLException se3) {
                se3.printStackTrace();
            }
            try {
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException se4) {
                se4.printStackTrace();
            }
        }
    }
}

在这个例子中,我们首先执行了一个SELECT查询,查询出所有工资大于50000的员工信息。然后,我们插入了一跳新的员工信息,工资为60000.接着,我们再次执行了相同的SELECT查询,这次查询出来的结果中包含了新插入的员工信息。由于这个例子中的查询条件了salary>50000,因此新插入的员工信息也被查询出来了。这就是一个典型的幻读现象。

为了避免幻读的发生,我们需要使用更高的隔离级别,例如可重复读,串行化。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值