链接
数据库事务隔离级别“读未提交(Read Uncommitted)”代码演示
数据库事务隔离级别“读已提交(Read Committed)”代码演示
数据库事务隔离级别“可重复读(Repeatable Read)”代码演示
数据库事务隔离级别“串行化(Serializable)”代码演示
目录
一、简介
数据库事务的隔离级别用于控制多个事务并发访问数据库时的相互影响程度,不同的隔离级别在性能和数据一致性上有不同的权衡。SQL 标准定义了四种隔离级别,从低到高分别为:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
读已提交(Read Committed)是比读未提交(Read Uncommitted)稍高一点的事务隔离级别,它确保一个事务只能读取到其他事务已经提交的数据。这种隔离级别避免了“脏读”问题,即事务不会读取到其他事务尚未提交的更改。尽管如此,读已提交隔离级别仍然存在一些数据一致性问题,比如不可重复读现象(后面将通过代码进行演示),即在同一个事务中多次读取相同的数据可能会得到不同的结果。
1. 优点
比读未提交级别提供了更好的数据一致性:读已提交隔离级别确保了一个事务只能读取到其他事务已经提交的数据,从而避免了脏读问题。这意味着事务读取到的数据是可靠的,并且这些数据已经被其他事务确认为有效和持久化的。因此,读已提交隔离级别在数据一致性和可靠性方面比读未提交隔离级别表现更好。
2. 缺点
存在不可重复读的问题:尽管读已提交隔离级别避免了脏读,但它无法解决不可重复读的问题。不可重复读是指在一个事务中,如果同一查询在不同时间点执行,可能会返回不同的结果。例如,在第一次读取账户余额后,另一个事务可能修改并提交了该余额,导致后续读取的结果与第一次不同。对于一些对数据一致性要求较高的场景,这可能无法满足需求。
3. 示例描述
假设事务 A 在第一次读取账户余额时,得到的是 100 元。此时,事务 B 将账户余额从 100 元修改为 200 元并提交了更改。当事务 A 再次读取该账户余额时,得到的结果是 200 元。这就导致了不可重复读现象,因为事务 A 在同一事务中的两次读取操作得到了不同的结果。尽管这种行为避免了脏读问题,但对于某些需要严格数据一致性的应用场景来说,这种不可重复读的情况可能会引发问题。
二、代码示例
在该示例中,我们不会演示读已提交(Read Committed)是如何避免脏读现象的,而是重点演示这种隔离级别存在的问题,即:不可重复读现象。
我们重点关注客户端A的事务隔离级别,将在客户端A的代码中演示读已提交(Read Committed)这种隔离级别存在的不可重复读现象(客户端B采用什么样的隔离级别,并不会影响客户端A中出现不可重复度现象,因为A中的事务隔离级别是Read Committed)。
C# 版本
客户端A的代码
客户端A会启动一个事务,第一次读取账户余额为100元,然后在事务B提交后再次读取账户余额,发现余额变为200元,展示了不可重复读现象。
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,设置隔离级别为ReadCommitted
SqlTransaction transactionA = connection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);
try
{
SqlCommand commandA = connection.CreateCommand();
commandA.Transaction = transactionA;
// 第一次查询账户余额
commandA.CommandText = "SELECT Balance FROM Accounts WHERE AccountId = 1";
object result = commandA.ExecuteScalar();
Console.WriteLine($"Client A: 第一次读取到的账户余额是 {result} 元"); // 应输出100元
// 模拟等待一段时间让Client B有机会更新数据
System.Threading.Thread.Sleep(5000); // 等待5秒
// 再次查询账户余额
result = commandA.ExecuteScalar();
Console.WriteLine($"Client A: 第二次读取到的账户余额是 {result} 元"); // 应输出200元
transactionA.Commit(); // 提交事务A
}
catch (Exception ex)
{
Console.WriteLine("Client A异常: " + ex.Message);
transactionA.Rollback(); // 如果发生异常,回滚事务
}
}
}
}
执行结果输出:
- Client A: 第一次读取到的账户余额是 100 元
- Client A: 第二次读取到的账户余额是 200 元
执行结果表明事务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,设置隔离级别为ReadCommitted
SqlTransaction transactionB = connection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);
try
{
SqlCommand commandB = connection.CreateCommand();
commandB.Transaction = transactionB;
// 更新账户余额为200元
commandB.CommandText = "UPDATE Accounts SET Balance = 200 WHERE AccountId = 1";
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元,然后在事务B提交后再次读取账户余额,发现余额变为200元,展示了不可重复读现象。
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);
// 设置隔离级别为ReadCommitted
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
conn.setAutoCommit(false);
stmt = conn.createStatement();
// 第一次查询账户余额
ResultSet rs = stmt.executeQuery("SELECT Balance FROM Accounts WHERE AccountId = 1");
if (rs.next()) {
int balance = rs.getInt("Balance");
System.out.println("Client A: 第一次读取到的账户余额是 " + balance + " 元"); // 应输出100元
}
// 模拟等待一段时间让Client B有机会更新数据
Thread.sleep(5000); // 等待5秒
// 再次查询账户余额
rs = stmt.executeQuery("SELECT Balance FROM Accounts WHERE AccountId = 1");
if (rs.next()) {
int balance = rs.getInt("Balance");
System.out.println("Client A: 第二次读取到的账户余额是 " + balance + " 元"); // 应输出200元
}
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: 第一次读取到的账户余额是 100 元
- Client A: 第二次读取到的账户余额是 200 元
执行结果表明事务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);
// 设置隔离级别为ReadCommitted
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
conn.setAutoCommit(false);
stmt = conn.createStatement();
// 更新账户余额为200元
stmt.executeUpdate("UPDATE Accounts SET Balance = 200 WHERE AccountId = 1");
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元