引言

在数据库系统中,事务是一组不可分割的操作序列,这些操作要么全部成功执行,要么全部失败回滚。然而,在多事务并发执行的环境下,会出现一些数据不一致的问题,其中不可重复读、幻读和脏读是比较常见的三种情况。本文将详细介绍这三种问题的概念、产生原因,并给出基于 Java 的解决方案。

一、不可重复读、幻读、脏读的概念及产生原因

1. 脏读(Dirty Read)

  • 概念:一个事务读取到了另一个未提交事务修改的数据。当一个事务对数据进行修改但还未提交时,另一个事务读取了这个被修改但未确定的数据,如果前一个事务回滚,那么后一个事务读取到的数据就是无效的 “脏数据”。
  • 产生原因:在低隔离级别下,事务之间没有足够的隔离机制来阻止一个事务读取另一个未提交事务的中间结果。

2. 不可重复读(Non - Repeatable Read)

  • 概念:在同一个事务中,多次读取同一数据时,由于其他事务对该数据进行了修改并提交,导致读取结果不一致。也就是说,在事务执行过程中,两次读取同一行数据可能得到不同的值。
  • 产生原因:并发事务对同一行数据进行修改和读取操作,且没有适当的隔离措施来保证事务内数据的一致性。

3. 幻读(Phantom Read)

  • 概念:在同一个事务中,两次执行相同的查询语句,由于其他事务插入或删除了符合查询条件的记录,导致查询结果集的行数不同,就好像出现了 “幻影” 一样。
  • 产生原因:并发事务对数据进行插入或删除操作,而当前事务在执行过程中没有对这些可能的变化进行有效的控制。

二、示例代码展示问题

数据库表结构

假设我们有一个 users 表,用于存储用户信息,表结构如下:

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT
);

脏读示例代码

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class DirtyReadExample {
    public static void main(String[] args) {
        try {
            // 模拟事务 A
            Thread transactionA = new Thread(() -> {
                try (Connection connectionA = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
                    connectionA.setAutoCommit(false);
                    try (Statement statementA = connectionA.createStatement()) {
                        // 修改数据但不提交
                        statementA.executeUpdate("UPDATE users SET age = 30 WHERE id = 1");
                        Thread.sleep(5000);
                        // 回滚事务
                        connectionA.rollback();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            // 模拟事务 B
            Thread transactionB = new Thread(() -> {
                try (Connection connectionB = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
                    connectionB.setAutoCommit(false);
                    try (Statement statementB = connectionB.createStatement()) {
                        // 读取数据
                        ResultSet resultSet = statementB.executeQuery("SELECT age FROM users WHERE id = 1");
                        if (resultSet.next()) {
                            System.out.println("事务 B 读取到的年龄: " + resultSet.getInt("age"));
                        }
                        connectionB.commit();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            transactionA.start();
            Thread.sleep(1000);
            transactionB.start();

            transactionA.join();
            transactionB.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

不可重复读示例代码

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class NonRepeatableReadExample {
    public static void main(String[] args) {
        try {
            // 模拟事务 A
            Thread transactionA = new Thread(() -> {
                try (Connection connectionA = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
                    connectionA.setAutoCommit(false);
                    try (Statement statementA = connectionA.createStatement()) {
                        // 第一次读取数据
                        ResultSet resultSet1 = statementA.executeQuery("SELECT age FROM users WHERE id = 1");
                        if (resultSet1.next()) {
                            System.out.println("事务 A 第一次读取到的年龄: " + resultSet1.getInt("age"));
                        }
                        Thread.sleep(5000);
                        // 第二次读取数据
                        ResultSet resultSet2 = statementA.executeQuery("SELECT age FROM users WHERE id = 1");
                        if (resultSet2.next()) {
                            System.out.println("事务 A 第二次读取到的年龄: " + resultSet2.getInt("age"));
                        }
                        connectionA.commit();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            // 模拟事务 B
            Thread transactionB = new Thread(() -> {
                try (Connection connectionB = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
                    connectionB.setAutoCommit(false);
                    try (Statement statementB = connectionB.createStatement()) {
                        // 修改数据并提交
                        statementB.executeUpdate("UPDATE users SET age = 30 WHERE id = 1");
                        connectionB.commit();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            transactionA.start();
            Thread.sleep(1000);
            transactionB.start();

            transactionA.join();
            transactionB.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

幻读示例代码

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class PhantomReadExample {
    public static void main(String[] args) {
        try {
            // 模拟事务 A
            Thread transactionA = new Thread(() -> {
                try (Connection connectionA = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
                    connectionA.setAutoCommit(false);
                    try (Statement statementA = connectionA.createStatement()) {
                        // 第一次查询数据
                        ResultSet resultSet1 = statementA.executeQuery("SELECT * FROM users WHERE age > 25");
                        int count1 = 0;
                        while (resultSet1.next()) {
                            count1++;
                        }
                        System.out.println("事务 A 第一次查询到的记录数: " + count1);
                        Thread.sleep(5000);
                        // 第二次查询数据
                        ResultSet resultSet2 = statementA.executeQuery("SELECT * FROM users WHERE age > 25");
                        int count2 = 0;
                        while (resultSet2.next()) {
                            count2++;
                        }
                        System.out.println("事务 A 第二次查询到的记录数: " + count2);
                        connectionA.commit();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            // 模拟事务 B
            Thread transactionB = new Thread(() -> {
                try (Connection connectionB = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
                    connectionB.setAutoCommit(false);
                    try (Statement statementB = connectionB.createStatement()) {
                        // 插入新记录并提交
                        statementB.executeUpdate("INSERT INTO users (id, name, age) VALUES (2, 'John', 30)");
                        connectionB.commit();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });

            transactionA.start();
            Thread.sleep(1000);
            transactionB.start();

            transactionA.join();
            transactionB.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三、Java 解决方案

1. 解决脏读

可以通过设置事务的隔离级别为 READ COMMITTED,该隔离级别保证一个事务只能读取到其他事务已经提交的数据,从而避免脏读。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class SolveDirtyRead {
    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
            // 设置事务隔离级别为 READ COMMITTED
            connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            connection.setAutoCommit(false);
            try (Statement statement = connection.createStatement()) {
                ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
                while (resultSet.next()) {
                    System.out.println(resultSet.getString("name"));
                }
                connection.commit();
            } catch (Exception e) {
                connection.rollback();
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2. 解决不可重复读

可以将事务的隔离级别设置为 REPEATABLE READ,该隔离级别保证在同一个事务中,多次读取同一数据时结果是一致的,避免了不可重复读的问题。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class SolveNonRepeatableRead {
    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
            // 设置事务隔离级别为 REPEATABLE READ
            connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
            connection.setAutoCommit(false);
            try (Statement statement = connection.createStatement()) {
                ResultSet resultSet1 = statement.executeQuery("SELECT age FROM users WHERE id = 1");
                if (resultSet1.next()) {
                    System.out.println("第一次读取到的年龄: " + resultSet1.getInt("age"));
                }
                // 模拟其他事务可能的修改操作
                Thread.sleep(5000);
                ResultSet resultSet2 = statement.executeQuery("SELECT age FROM users WHERE id = 1");
                if (resultSet2.next()) {
                    System.out.println("第二次读取到的年龄: " + resultSet2.getInt("age"));
                }
                connection.commit();
            } catch (Exception e) {
                connection.rollback();
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. 解决幻读

  • 使用串行化隔离级别(SERIALIZABLE):串行化隔离级别是最高的隔离级别,它会对事务进行串行执行,即一个事务执行完后另一个事务才能开始执行,避免了幻读问题。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class SolvePhantomReadWithSerializable {
    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
            // 设置事务隔离级别为 SERIALIZABLE
            connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
            connection.setAutoCommit(false);
            try (Statement statement = connection.createStatement()) {
                ResultSet resultSet1 = statement.executeQuery("SELECT * FROM users WHERE age > 25");
                int count1 = 0;
                while (resultSet1.next()) {
                    count1++;
                }
                System.out.println("第一次查询到的记录数: " + count1);
                // 模拟其他事务可能的插入操作
                Thread.sleep(5000);
                ResultSet resultSet2 = statement.executeQuery("SELECT * FROM users WHERE age > 25");
                int count2 = 0;
                while (resultSet2.next()) {
                    count2++;
                }
                System.out.println("第二次查询到的记录数: " + count2);
                connection.commit();
            } catch (Exception e) {
                connection.rollback();
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  • 使用当前读和行锁(基于 MVCC 的数据库,如 MySQL InnoDB):在 MySQL InnoDB 中,使用 SELECT ... FOR UPDATE 可以对读取的记录加上行锁,结合 MVCC 机制避免幻读。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class SolvePhantomReadWithCurrentRead {
    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb", "root", "password")) {
            connection.setAutoCommit(false);
            try (Statement statement = connection.createStatement()) {
                // 使用当前读,对查询结果加行锁
                ResultSet resultSet1 = statement.executeQuery("SELECT * FROM users WHERE age > 25 FOR UPDATE");
                int count1 = 0;
                while (resultSet1.next()) {
                    count1++;
                }
                System.out.println("第一次查询到的记录数: " + count1);
                // 模拟其他事务可能的插入操作
                Thread.sleep(5000);
                ResultSet resultSet2 = statement.executeQuery("SELECT * FROM users WHERE age > 25 FOR UPDATE");
                int count2 = 0;
                while (resultSet2.next()) {
                    count2++;
                }
                System.out.println("第二次查询到的记录数: " + count2);
                connection.commit();
            } catch (Exception e) {
                connection.rollback();
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

四、总结

不可重复读、幻读和脏读是数据库多事务并发执行时常见的问题,这些问题会影响数据的一致性和正确性。通过合理设置事务的隔离级别和使用合适的锁机制,我们可以在 Java 程序中有效地解决这些问题。在实际应用中,需要根据具体的业务场景和性能需求来选择合适的解决方案,以达到数据一致性和系统性能的平衡。例如,串行化隔离级别虽然能解决所有问题,但会严重影响并发性能,而较低的隔离级别可能会带来数据不一致的风险,需要开发者根据实际情况进行权衡。

Logo

一站式 AI 云服务平台

更多推荐