链接
数据库事务隔离级别“读未提交(Read Uncommitted)”代码演示
数据库事务隔离级别“读已提交(Read Committed)”代码演示
数据库事务隔离级别“可重复读(Repeatable Read)”代码演示
数据库事务隔离级别“串行化(Serializable)”代码演示
目录
一、简介
数据库事务的隔离级别用于控制多个事务并发访问数据库时的相互影响程度,不同的隔离级别在性能和数据一致性上有不同的权衡。SQL 标准定义了四种隔离级别,从低到高分别为:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
可重复读(Repeatable Read)是一种事务隔离级别,它确保在一个事务中多次读取同一数据时,结果是一致的。这种隔离级别通过锁定机制防止其他事务在当前事务完成之前修改已读取的数据,从而避免了不可重复读问题。然而,尽管它可以保证数据的一致性,但在某些情况下,仍然存在幻读问题(后面将通过代码进行演示),并且由于需要对读取的数据进行锁定,可能会影响系统的并发性能。
1. 优点
提供了较高的数据一致性:可重复读隔离级别保证了在一个事务内多次读取同一数据的结果是一致的。这意味着,如果一个事务在某个时间点读取了一组数据,在该事务结束之前,即使其他事务对该数据进行了修改并提交,当前事务再次读取这些数据时,依然会得到与第一次相同的结果。这大大提高了数据的一致性和可靠性,特别适用于对数据一致性要求较高的应用场景。
2. 缺点
存在幻读问题,并且由于需要对读取的数据进行锁定,可能会降低并发性能:尽管可重复读隔离级别可以避免不可重复读问题,但它无法完全解决幻读问题。幻读是指在一个事务中,当执行相同的查询条件时,可能会返回不同的结果集,因为其他事务插入了新的符合查询条件的数据并提交。此外,为了保证数据的一致性,可重复读隔离级别通常会对读取的数据进行锁定,这可能导致其他事务需要等待锁定释放,从而降低了系统的并发性能。
3. 示例描述
假设事务 A 第一次按照条件“账户余额大于 100 元”查询,得到了 10 条记录。此时,事务 B 插入了一条账户余额为 200 元的新记录并提交了更改。当事务 A 再次按照相同的条件查询时,发现查询结果变成了 11 条记录。这就是幻读现象:尽管事务 A 在两次查询之间没有修改任何数据,但由于事务 B 插入了新的符合条件的数据,导致查询结果发生了变化。虽然事务 A 的查询结果一致地反映了其首次查询时的数据状态,但新的数据插入仍然影响了查询结果的一致性。
二、代码示例
在该示例中,我们重点关注客户端A的事务隔离级别,将在客户端A的代码中演示可重复读(Repeatable Read)这种隔离级别存在的幻读(客户端B采用什么样的隔离级别,并不会影响客户端A中出现幻读现象,因为A中的事务隔离级别是Repeatable Read)。
C# 版本
客户端A的代码
客户端A会启动一个事务,在事务中两次按照条件“账户余额大于100元”查询数据,展示幻读现象。
using System;
using System.Data.SqlClient;
class ClientA
{
static void Main(string[] args)
{
string connectionString = "your_connection_string_here"; // 替换为你的数据库连接字符串
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
// 开始事务A,设置隔离级别为RepeatableRead
SqlTransaction transactionA = connection.BeginTransaction(System.Data.IsolationLevel.RepeatableRead);
try
{
SqlCommand commandA = connection.CreateCommand();
commandA.Transaction = transactionA;
// 第一次查询账户余额大于100元的记录
commandA.CommandText = "SELECT COUNT(*) FROM Accounts WHERE Balance > 100";
int countBefore = (int)commandA.ExecuteScalar();
Console.WriteLine($"Client A: 第一次查询到的记录数是 {countBefore} 条"); // 应输出10条
// 模拟等待一段时间让Client B有机会插入新记录
System.Threading.Thread.Sleep(5000); // 等待5秒
// 再次查询账户余额大于100元的记录
commandA.CommandText = "SELECT COUNT(*) FROM Accounts WHERE Balance > 100";
int countAfter = (int)commandA.ExecuteScalar();
Console.WriteLine($"Client A: 第二次查询到的记录数是 {countAfter} 条"); // 应输出11条
transactionA.Commit(); // 提交事务A
}
catch (Exception ex)
{
Console.WriteLine("Client A异常: " + ex.Message);
transactionA.Rollback(); // 如果发生异常,回滚事务
}
}
}
}
执行结果输出:
- Client A: 第一次查询到的记录数是 10 条
- Client A: 第二次查询到的记录数是 11 条
执行结果表明事务A两次执行同一查询,得到的结果集却不一致,这就是幻读现象。
客户端B的代码
客户端B会在客户端A第一次读取账户余额之后,将账户余额修改为200元并提交。
using System;
using System.Data.SqlClient;
class ClientB
{
static void Main(string[] args)
{
string connectionString = "your_connection_string_here"; // 替换为你的数据库连接字符串
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
// 开始事务B,设置隔离级别为默认(这里可以使用任何隔离级别)
SqlTransaction transactionB = connection.BeginTransaction();
try
{
SqlCommand commandB = connection.CreateCommand();
commandB.Transaction = transactionB;
// 插入一条账户余额大于100元的新记录
commandB.CommandText = "INSERT INTO Accounts (AccountId, Balance) VALUES (11, 200)";
commandB.ExecuteNonQuery();
Console.WriteLine("Client B: 已插入一条账户余额为200元的新记录");
transactionB.Commit(); // 提交事务B
}
catch (Exception ex)
{
Console.WriteLine("Client B异常: " + ex.Message);
transactionB.Rollback(); // 如果发生异常,回滚事务
}
}
}
}
执行结果输出:
- Client B: 已插入一条账户余额为200元的新记录
Java 版本
客户端A的代码
客户端A会启动一个事务,在事务中两次按照条件“账户余额大于100元”查询数据,展示幻读现象。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class ClientA {
public static void main(String[] args) throws InterruptedException {
String url = "jdbc:mysql://localhost:3306/your_database"; // 替换为你的数据库连接字符串
String user = "root";
String password = "password";
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection(url, user, password);
// 设置隔离级别为RepeatableRead
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setAutoCommit(false);
stmt = conn.createStatement();
// 第一次查询账户余额大于100元的记录
ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM Accounts WHERE Balance > 100");
if (rs.next()) {
int countBefore = rs.getInt(1);
System.out.println("Client A: 第一次查询到的记录数是 " + countBefore + " 条"); // 应输出10条
}
// 模拟等待一段时间让Client B有机会插入新记录
Thread.sleep(5000); // 等待5秒
// 再次查询账户余额大于100元的记录
rs = stmt.executeQuery("SELECT COUNT(*) FROM Accounts WHERE Balance > 100");
if (rs.next()) {
int countAfter = rs.getInt(1);
System.out.println("Client A: 第二次查询到的记录数是 " + countAfter + " 条"); // 应输出11条
}
conn.commit(); // 提交事务A
} catch (SQLException e) {
e.printStackTrace();
if (conn != null) {
try {
System.err.print("Transaction is being rolled back");
conn.rollback();
} catch (SQLException excep) {
excep.printStackTrace();
}
}
} finally {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
/* ignored */
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
/* ignored */
}
}
}
}
}
执行结果输出:
- Client A: 第一次查询到的记录数是 10 条
- Client A: 第二次查询到的记录数是 11 条
执行结果表明事务A两次执行同一查询,得到的结果集却不一致,这就是幻读现象。
客户端B的代码
客户端B会在客户端A第一次读取账户余额之后,将账户余额修改为200元并提交。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class ClientB {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/your_database"; // 替换为你的数据库连接字符串
String user = "root";
String password = "password";
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection(url, user, password);
// 设置隔离级别为默认(这里可以使用任何隔离级别)
conn.setAutoCommit(false);
stmt = conn.createStatement();
// 插入一条账户余额大于100元的新记录
stmt.executeUpdate("INSERT INTO Accounts (AccountId, Balance) VALUES (11, 200)");
System.out.println("Client B: 已插入一条账户余额为200元的新记录");
conn.commit(); // 提交事务B
} catch (SQLException e) {
e.printStackTrace();
if (conn != null) {
try {
System.err.print("Transaction is being rolled back");
conn.rollback();
} catch (SQLException excep) {
excep.printStackTrace();
}
}
} finally {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
/* ignored */
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
/* ignored */
}
}
}
}
}
执行结果输出:
- Client B: 已插入一条账户余额为200元的新记录