数据库事务的四种隔离级别

数据库事务的四种隔离级别

引言

事务

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

  • 读未提交(Read Uncommitted):事务中的修改可以被其他事务读取,即一个事务可以读取到另一个未提交事务修改的数据。

简而言之:一个事务可以到其他事务修改了但未提交的数据。

  • 读已提交(Read Committed):事务只能读取已经提交的数据,不能读取未提交的数据。在该隔离级别下,事务只能读取到已经提交的数据,因此会避免脏读的情况。(脏读的概念可以参考本栏其他博客)

简而言之:数据的读取只能读取已经提交过的数据,和读未提交相比,读未提交可以读取修改了单位提交的数据。而读已提交则不行,因此避免了脏读的情况。

  • 可重复读(Repeatable Read):在一个事务中多次读取同一个数据时,能够保证读取到的数据一致,即使其他事务修改了该数据。在隔离级别下,事务在读取数据时会锁定该数据,其他事务不能修改该数据,因此可以避免脏读和不可重复读的情况。

本人理解:应该用锁将写操作锁定,可以重复读取且数据保持一致。

  • 串行化(Serializable,序列化):最高的隔离级别,它保证所有事务之间的执行顺序按照某个顺序执行,避免了所有并发问题。在该隔离级别下,事务之间互相等待,直到前一个事务执行完成后才能执行下一个事务,因此可以避免脏读、不可重复读和幻读的情况。

将事务串行化,一次只能按照特定顺序执行一个事务,因为只执行一个事务,会避免很多问题,但是肯定会降低执行效率。


读未提交(Read Uncommitted)

读未提交的隔离级别意味着一个事务可以读取另一个未提交事务的修改,因此在读未提交的情况下,可能会出现脏读的问题。

读未提交相关的简单Java代码可以参考如下,已经做了很详细的注释。

// 假设有一个账户表 Account,有两个字段:id 和 balance

// 在一个事务中,将 id 为 1 的账户余额增加 100
Connection conn1 = DriverManager.getConnection(url, username, password);
//关掉自动提交,因为需要在读未提交的状态下,另一个事务读取未提交事务的修改。
conn1.setAutoCommit(false);
PreparedStatement pstmt1 = conn1.prepareStatement("UPDATE Account SET balance = balance + 100 WHERE id = 1");
pstmt1.executeUpdate();

// 在另一个事务中,读取 id 为 1 的账户余额,此时可以读到未提交事务的修改
Connection conn2 = DriverManager.getConnection(url, username, password);
conn2.setAutoCommit(false);
PreparedStatement pstmt2 = conn2.prepareStatement("SELECT balance FROM Account WHERE id = 1");
ResultSet rs = pstmt2.executeQuery();
if (rs.next()) {
    System.out.println("balance: " + rs.getInt("balance"));
}

在上述代码中,第一个事务对账户表的某个账户余额进行了修改,并提交了事务。在第二个事务中,查询了同一个账户的余额,此时可以读取到第一个事务修改后的结果。

然而如果再第一个事务中调用’conn1.rollback()‘方法回滚事务,那么在第二个事务中读取到的余额将是未提交事务前的余额。此说明在读未提交的隔离级别下,读取的数据可能是不确定的,因此需要谨慎使用该隔离级别。


读已提交(Read Committed)

在读已提交隔离级别下,一个事务只能读取已经提交的数据,因此不会出现脏读的问题,

以下是一个简单的Java代码示例,演示了读未提交隔离级别:

// 假设有一个账户表 Account,有两个字段:id 和 balance

// 在一个事务中,将 id 为 1 的账户余额增加 100
Connection conn1 = DriverManager.getConnection(url, username, password);
//关闭自动提交
conn1.setAutoCommit(false);
PreparedStatement pstmt1 = conn1.prepareStatement("UPDATE Account SET balance = balance + 100 WHERE id = 1");
pstmt1.executeUpdate();
conn1.commit();

// 在另一个事务中,读取 id 为 1 的账户余额,此时只能读取到已提交的修改
Connection conn2 = DriverManager.getConnection(url, username, password);
conn2.setAutoCommit(false);
conn2.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 设置隔离级别为读提交
PreparedStatement pstmt2 = conn2.prepareStatement("SELECT balance FROM Account WHERE id = 1");
ResultSet rs = pstmt2.executeQuery();
if (rs.next()) {
    System.out.println("balance: " + rs.getInt("balance"));
}

在上述代码中,第一个事务对账户表中的某个账户余额进行了修改,并提交了事务。在第二个事务中,查询了同一个账户的余额,由于设置了读提交隔离级别,因此只能读取到已经提交的修改结果。因此,无论在第一个事务是否提交或者回滚,第二个事务都只会读取到已提交的数据。

需要注意的是,虽然读提交隔离级别避免了脏读问题,但是仍然可能出现不可重复读和幻读问题。如果需要避免这些问题,可以选择更高的隔离级别,如可重复读或串行化。

第一个事务需要提交,不然第二个事务读取不到数据。


可重复读(Repeatable Read)

在可重复读隔离级别下,一个事务内多次读取同一数据,将得到一致的结果,即事务读取的数据与其他事务修改的数据是分离的。一下是一个简单的Java代码示例,演示了可重复读隔离级别:

// 假设有一个账户表 Account,有两个字段:id 和 balance

// 在一个事务中,读取 id 为 1 的账户余额,记录当前余额
Connection conn1 = DriverManager.getConnection(url, username, password);
//关闭自动提交
conn1.setAutoCommit(false);
conn1.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // 设置隔离级别为可重复读
PreparedStatement pstmt1 = conn1.prepareStatement("SELECT balance FROM Account WHERE id = 1");
ResultSet rs1 = pstmt1.executeQuery();
int balance1 = 0;
if (rs1.next()) {
    balance1 = rs1.getInt("balance");
}
//事务一没有提交,conn1.commit()

// 在另一个事务中,将 id 为 1 的账户余额增加 100
Connection conn2 = DriverManager.getConnection(url, username, password);
conn2.setAutoCommit(false);
PreparedStatement pstmt2 = conn2.prepareStatement("UPDATE Account SET balance = balance + 100 WHERE id = 1");
pstmt2.executeUpdate();
conn2.commit(); //事务二提交了

// 在第一个事务中,再次读取 id 为 1 的账户余额,与之前记录的余额进行比较
PreparedStatement pstmt3 = conn1.prepareStatement("SELECT balance FROM Account WHERE id = 1");
ResultSet rs2 = pstmt3.executeQuery();
int balance2 = 0;
if (rs2.next()) {
    balance2 = rs2.getInt("balance");
}
if (balance1 == balance2) {
    System.out.println("Balance is consistent");
} else {
    System.out.println("Balance is inconsistent");
}

在上述代码中,第一个事务中读取了一个账户的余额,记录了当前余额,并提交了事务。在第二个事务中对同一个账户的余额进行了修改,并提交了事务。在第一个事务中再次读取同一个账户的余额,与之前记录的余额进行比较,如果两次读取的结果相同,则打印“Balance is consistent”,否则打印”Balance is inconsistent”。

由于设置了可重复读隔离级别,第一个事务的两次读取操作将得到一致的结果。因此,无论在第二个事务是否提交或回滚,第一个事务都只会读取到第一次读取的余额,而不会读取到其他事务的修改结果。


串行化(Serializable)

在串行化隔离级别下,所有并发事务都会被强制串行执行,这意味着任何时候只有一个事务能够访问数据库。

以下是一个简单的Java代码示例,演示了串行化隔离级别:

// 假设有一个账户表 Account,有两个字段:id 和 balance

// 在一个事务中,读取 id 为 1 的账户余额,记录当前余额
Connection conn1 = DriverManager.getConnection(url, username, password);
conn1.setAutoCommit(false);
conn1.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // 设置隔离级别为串行化
PreparedStatement pstmt1 = conn1.prepareStatement("SELECT balance FROM Account WHERE id = 1 FOR UPDATE");
ResultSet rs1 = pstmt1.executeQuery();
int balance1 = 0;
if (rs1.next()) {
    balance1 = rs1.getInt("balance");
}

// 在另一个事务中,试图对同一个账户的余额进行修改,但是因为第一个事务已经获得了排他锁,所以会阻塞等待
Connection conn2 = DriverManager.getConnection(url, username, password);
conn2.setAutoCommit(false);
conn2.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); // 设置隔离级别为串行化
PreparedStatement pstmt2 = conn2.prepareStatement("UPDATE Account SET balance = balance + 100 WHERE id = 1");
pstmt2.executeUpdate();

// 在第一个事务中,再次读取 id 为 1 的账户余额,与之前记录的余额进行比较
PreparedStatement pstmt3 = conn1.prepareStatement("SELECT balance FROM Account WHERE id = 1");
ResultSet rs2 = pstmt3.executeQuery();
int balance2 = 0;
if (rs2.next()) {
    balance2 = rs2.getInt("balance");
}
if (balance1 == balance2) {
    System.out.println("Balance is consistent");
} else {
    System.out.println("Balance is inconsistent");
}

// 提交第一个事务
conn1.commit();

在上述代码中,第一个事务读取了一个账户余额,记录了当前余额,并获得了一个排它锁。在第二个事务中试图对同一个账户的余额进行修改,但因为第一个事务已经获得了排它锁,所以会阻塞等待。在第一个事务中再次读取同一个账户的余额,与之前记录的余额进行比较,如果两次读取的结果相同,则打印"Balance is consistent",否则打印 “Balance is inconsistent”。

由于设置了串行化隔离级别,第二个事务会阻塞等待第一个事务执行完毕后才能执行,因此无论在第二个事务执行前还是执行后,第一个事务所读取的数据都不会受到第二个事务的影响。当第一个事务执行完毕并提交后,第二个事务才会继续执行。

四种隔离级别排序

数据库四种隔离级别按照数据一致性和性能的权衡不同,可以按照以下顺序排序:

  1. 串行化(Serializable):最严格的隔离级别,保证了数据的最高一致性,但是牺牲了并发性能。
  2. 可重复读(Repeatable Read):保证了事务之间不会相互干扰,但是可能会出现幻读问题,读取到未提交的新插入的行。
  3. 读已提交(Read Committed):保证了读取到的数据都是已经提交的,避免了脏读问题,但是可能会出现不可重复读和幻读问题。
  4. 读未提交(Read Uncommitted):最宽松的隔离级别,允许事务读取到其他未提交的数据,可能会出现脏读、不可重复读和幻读等问题。

需要注意的是,隔离级别的选择应该是根据具体的应用场景和对数据一致性和性能的需求进行权衡,在高并发场景下,需要更高的并发性能,但是也要保证数据的一致性,因此需要根据具体情况选择适当的隔离级别。

  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值