事务隔离级别与表锁的关系

123

我已经了解了四个隔离级别:

Isolation Level       Dirty Read    Nonrepeatable Read  Phantom Read  
READ UNCOMMITTED      Permitted       Permitted           Permitted
READ COMMITTED              --        Permitted           Permitted
REPEATABLE READ             --             --             Permitted
SERIALIZABLE                --             --              --

我想了解每种事务隔离级别对表所施加的锁定作用。

READ UNCOMMITTED - no lock on table
READ COMMITTED - lock on committed data
REPEATABLE READ - lock on block of sql(which is selected by using select query)
SERIALIZABLE - lock on full table(on which Select query is fired)

以下是事务隔离中可能发生的三种现象:

脏读- 无锁

不可重复读- 在提交的数据上加锁,避免脏读

幻读- 对选择某个SQL块(使用select查询)进行锁定

我想了解这些隔离级别在哪里定义:仅在JDBC / Hibernate级别还是也在数据库中定义

备注:我已查看Oracle中的隔离级别链接,但它们看起来很混乱,并且只讨论特定于数据库的内容


5
这完全取决于数据库。不同的数据库可能会使用不同的隔离级别算法。有些数据库可能会使用MVCC(在选择查询上不加锁),有些则使用严格的两阶段锁定(共享和排他锁)。 - brb tea
4个回答

178
我希望了解每个事务隔离级别对表所施加的锁的情况。例如,您有三个并发进程A、B和C。A开始一个事务,写入数据并提交/回滚(取决于结果)。B只执行一个SELECT语句来读取数据。C读取和更新数据。所有这些进程都在同一张表T上工作。
  • READ UNCOMMITTED - 数据表无锁。你可以在写入数据的同时读取表中的数据。这意味着A写入的数据(未提交)可以被B读取并用于任何目的,即使A执行回滚,B仍然可以读取并使用数据。这是最快但最不安全的处理数据的方式,因为它可能导致在不相关的物理表中产生数据空洞(是的,在实际应用程序中两个表可以是逻辑上但不是物理上相关的)。
  • READ COMMITTED - 锁定已提交的数据。你只能读取已提交的数据。这意味着A写入数据后,B无法读取A保存的数据,直到A执行提交。问题在于C可以更新由B读取和使用的数据,而B客户端将没有更新的数据。
  • REPEATABLE READ - 锁定一块SQL(通过使用select查询选择)。这意味着B在某些条件下读取数据,例如WHERE aField > 10 AND aField < 20,A插入aField值介于10和20之间的数据,然后B再次读取数据并得到不同的结果。
  • SERIALIZABLE - 锁定整个表(在执行Select查询时)。这意味着B读取数据并且没有其他事务可以修改表上的数据。这是处理数据最安全但最慢的方式。此外,由于简单的读取操作锁定了整个表,这可能会在生产环境中导致严重问题:想象一下T表是发票表,用户X想知道当天的发票,而用户Y想创建新的发票,因此当X执行发票读取时,Y无法添加新的发票(当涉及到金钱时,人们会变得非常生气,特别是老板们)。

我想了解我们是在JDBC / Hibernate级别还是在数据库中定义这些隔离级别。

使用JDBC,您可以使用Connection#setTransactionIsolation进行定义。

使用Hibernate:

<property name="hibernate.connection.isolation">2</property>

在哪里?

  • 1:读取未提交的数据(READ UNCOMMITTED)
  • 2:读取已提交的数据(READ COMMITTED)
  • 4:可重复读取的数据(REPEATABLE READ)
  • 8:串行化的数据(SERIALIZABLE)

Hibernate配置可以从这里获取(抱歉,它是用西班牙语写的)。

顺便说一下,您也可以在RDBMS上设置隔离级别:

等等……


2
此外,为了节省每个事务都以SET TRANSACTION语句开始的网络和处理成本,您可以使用ALTER SESSION语句为所有后续事务设置事务隔离级别: ALTER SESSION SET ISOLATION_LEVEL SERIALIZABLE; ALTER SESSION SET ISOLATION_LEVEL READ COMMITTED; - lowLatency
16
关于可重复读(REPEATABLE READ)- 我认为一个更好的例子来展示它是如下:B开始一个事务,在SQL块中读取WHERE aField > 10 AND aField < 20的数据,该数据在事务结束之前被锁定。A试图更新那些数据但因为锁而等待。现在当B在同一事务中再次读取那些数据时,可以保证读到相同的数据,因为它被锁定了。如果我有误请纠正我。 - BornToCode
1
@LuiggiMendoza 一般概念来说,隔离级别只涉及到脏读不可重复读幻读。锁(S2PL)或MVCC是不同供应商的实现。 - brb tea
4
@LuiggiMendoza - 我之前说的不太准确,应该是这样的 - B读取的数据并没有改变,但是B后续选择可能会返回更多行。这是因为A不能修改B已经读取的行,直到A释放它们为止。但是A可以插入符合where条件的新行(因此下一次A执行选择时,将获得一个具有更多行的不同结果 - 幻读)。 - BornToCode
1
@NitinBansal - 是的,那是个打字错误。应该是“A 不能修改 B 已经读取的行,直到 B 释放它们”。 - BornToCode
显示剩余7条评论

11

如brb tea所说,这取决于数据库实现和他们使用的算法: MVCC或两阶段锁定。

CUBRID (开源关系型数据库管理系统) 解释了这两种算法的思想:

  • 两阶段锁定(2PL)

第一种方法是当T2事务试图更改A记录时,它知道T1事务已经更改了A记录,并等待T1事务完成,因为T2事务无法知道T1事务是否提交或回滚。这种方法称为两阶段锁定(2PL)。

  • 多版本并发控制(MVCC)

另一种方法是允许T1和T2事务各自拥有其自己的更改版本。即使T1事务将A记录从1更改为2,T1事务仍将原始值1保留不变,并写入T1事务版本的A记录为2。然后,以下T2事务将A记录从1更改为3,而不是从2更改为4,并写入T2事务版本的A记录为3。

当T1事务回滚时,如果2,即T1事务版本,未应用于A记录,则无关紧要。之后,如果T2事务提交,则3,即T2事务版本,将应用于A记录。如果T1事务先于T2事务提交,则A记录将更改为2,然后在提交T2事务时更改为3。最终数据库状态与独立执行每个事务的状态相同,没有对其他事务产生任何影响。因此,它满足ACID属性。这种方法称为多版本并发控制(MVCC)。

MVCC允许并发修改,但会增加内存开销(因为必须维护相同数据的不同版本)和计算开销(在REPETEABLE_READ级别中,您不能丢失更新,因此必须检查数据的版本,就像Hiberate使用 Optimistic Locking一样)。
在2PL中,事务隔离级别控制以下内容
无论读取数据时是否采取锁以及请求的锁类型。 持有读取锁的时间有多长。 引用另一个事务修改的行的读操作: 阻塞,直到该行上的排他锁被释放。 检索在语句或事务开始时存在的行的提交版本。 读取未提交的数据修改。
选择事务隔离级别不会影响获取保护数据修改的锁。无论为该事务设置了什么隔离级别,事务始终会对其修改的任何数据获得独占锁,并保持该锁直到事务完成。对于读操作,事务隔离级别主要定义了保护其他事务所做修改影响的级别。
较低的隔离级别增加了许多用户同时访问数据的能力,但也增加了用户可能遇到的并发效应,例如脏读或丢失更新。

SQL Server中,锁和隔离级别之间的关系有具体的例子(使用2PL,除了READ_COMMITED且READ_COMMITTED_SNAPSHOT=ON)。

  • READ_UNCOMMITED: 不会发出共享锁,以防止其他事务修改当前事务读取的数据。 READ UNCOMMITTED 事务也不会被排他锁阻止,这些锁将阻止当前事务读取已被其他事务修改但未提交的行。[...]

  • READ_COMMITED:

    • 如果 READ_COMMITTED_SNAPSHOT 设置为 OFF(默认值):使用共享锁来防止其他事务在当前事务运行读操作时修改行。共享锁还会阻止语句从已被其他事务修改的行中读取,直到其他事务完成为止。[...] 在处理下一行之前,行锁将被释放。[...]
    • 如果 READ_COMMITTED_SNAPSHOT 设置为 ON,则数据库引擎使用行版本控制,向每个语句提供一个事务一致的数据快照,该数据快照反映了语句开始时的数据状态。锁不用于保护数据免受其他事务的更新。
  • REPETEABLE_READ: 对事务中每个语句读取的所有数据都放置共享锁,并保持直到事务完成。

  • SERIALIZABLE: 在事务执行期间,范围锁定在与每个语句的搜索条件匹配的键值范围内。[...] 范围锁将保持到事务完成。


5

锁定始终在数据库层面上进行。

Oracle官方文档中可以了解到:

为避免事务冲突,数据库管理系统使用锁定机制,这些机制用于阻止其他人对正在被事务访问的数据进行访问。 (请注意,在自动提交模式下,每个语句都是一个事务,锁仅保留一个语句。)设置锁定后,该锁定一直有效,直到事务提交或回滚。例如,数据库管理系统可以锁定一个表的行,直到对其进行的更新已经提交。这个锁的效果是防止用户获得脏读取,也就是在数据变得持久之前读取值。(访问未提交的更新值被认为是“脏读取”,因为该值有可能回滚到以前的值。如果您读取稍后会回滚的值,则读取的值将无效。)

锁的设置方式由所谓的事务隔离级别(transaction isolation level)决定,它可以从完全不支持事务到支持强制执行非常严格的访问规则。

一个事务隔离级别的例子是 TRANSACTION_READ_COMMITTED,它将不允许在提交之前访问任何值。换句话说,如果将事务隔离级别设置为 TRANSACTION_READ_COMMITTED,则数据库管理系统不允许发生脏读取。接口 Connection 包括五个值,代表您可以在JDBC中使用的事务隔离级别。


0
关于“读已提交(READ COMMITTED)”——我认为一个更好的演示例子如下:
假设: 数据 a = 10; 线程 A、B、C
1. A 执行 A = A + 1 ==> A = 11; 同时,B 和 C 尝试更新该数据但因为锁而等待。
2. A 提交数据后,B 查询 A = 11。同时,C 更新 A = A + 1 = 12。问题在于,B 再次查询 A = 12,同一事务中存在不一致的两个查询结果。
如果我错了,请纠正我。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接