脏读、幻读和不可重复读
前言:
在多个并发事务并行执行时,如果多个事务同时读写,可能会出现脏读、幻读和不可重复读情况。
事务
数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单元,由一个有限的数据库操作序列构成。——维基百科
简而言之:一系列数据库操作语句组成事务。
脏读(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,因此新插入的员工信息也被查询出来了。这就是一个典型的幻读现象。
为了避免幻读的发生,我们需要使用更高的隔离级别,例如可重复读,串行化。