目录
2. 事务的特性
3. 数据库对事务的管理
4. 并发控制
4.1. 事务隔离
4.2. 显式锁定
同大多数的商业数据库一样,KingbaseES数据库也引入了事务的概念。本章介绍KingbaseES中的事务以及事务的管理。.本部分包含以下主题:
-
事务的概念
-
事务的特性
-
数据库对事务的管理
-
并发控制
1. 事务的概念
数据库中的事务是一个独立的逻辑单元,它包括了一个或多个 SQL语句。数据库工作的基本单位是事务,事务是不能被拆分的。一个事务执行的结果只可能有两个:事务内的语句都成功执行,事务执行成功,即事务提交;或事务执行失败,本事务不对数据造成任何影响,即事务回滚。为了更形象的描述事务,举一个简单的例子:银行数据库如何处理客户转账。一个客户有这样的要求:把一笔钱从 A 帐户转到 B 帐户。这个要求可以拆分为三个独立的部分:
-
帐户A划走这笔钱
-
帐户B增加这笔钱
-
记录这笔交易
整个交易过程数据库确保最终达到如下两种状态中的一个:
-
三条语句都成功执行,达到转账目的并且做交易记录。 整个事务提交,数据应用到数据库。
-
其中一条语句执行过程发生错误,比如 A 帐户资金不足,B帐户不存在,或者在整个交易过程中发生硬件故障,数据库停机。 整个事务回滚,执行成功的语句被撤销,执行失败的语句不对数据造成影响,还未执行的语句不再执行。数据回滚到此事务未开始的状态。 无论什么原因,整个业务逻辑不允许出现诸如帐户 A 上的钱被划走,而却没有到达帐户B,或者这个交易没有被记录这样的中间结果,整个交易必须是原子性的,要么整个操作成功,要么整个操作失败。数据库的事务概念正是吻合了此要求。
一条 SQL 语句成功执行标志着:
-
SQL语句被解析,语法正确。
-
SQL语句是有效的,语义正确。
-
一些数据可能被更改,但是这些更改不是永久性的,在此语句所在事务提交前,数据的更改并没有在整个数据库生效。
对于事务的执行结果,只能有提交和回滚。
-
提交操作意味着:事务内的语句对数据的改变被数据库接受。这将造成数据对于整个数据库发生永久性的改变。
-
回滚操作意味着:这个事务内所有对数据造成的更改全部被撤销。
一条 SQL 语句执行失败会回滚整个事务。
2. 事务的特性
为了保证数据库中的数据一致性,确保KingbaseES 能够在并发访问和系统发生故障时对数据进行维护,事务作为数据库工作的基本单位,具有下列四个特性,称为事务的 ACID 特性。
原子性(Atomicity)
一个事务对数据库的所有操作,是一个不可分割的工作单元。这些操作要么全部执行,要么什么也不做。
一致性(Consistency)
一个事务独立执行的结果应保持数据库的一致性,即数据不会因为事务的执行而遭受破坏。例如事务的概念里银行转账的例子,保证了数据的一致性。
隔离性(Isolation)
在多个事务并发执行时,系统应保证与这些事务先后单独执行时的结果一样,此时称事务达到了隔离性的要求,也就是在多个并发事务执行时,保证执行结果是正确的,如同单用户环境一样。隔离性是由KingbaseES 的并发控制子系统实现的。
持久性(Durability)
一个事务一旦完成全部操作后,它对数据库的所有更新应永久地反映在数据库中。即使以后系统发生故障,也应保留这个事务执行的痕迹。事务的持久性由 KingbaseES 的恢复管理子系统实现。
事务是恢复和并发控制的基本单位。KingbaseES 能够保证事务的 ACID 特性不被破坏,分为以下几种情况:
-
多个事务并发运行时,不同事务的操作交叉执行。KingbaseES保证多个事务的交叉运行不影响这些事务的原子性。
-
事务在运行过程中被强行终止。KingbaseES保证被强行终止的事务对数据库和其他事务没有任何影响。
3. 数据库对事务的管理
对于事务的管理,KingbaseES 支持事务的隐式和显式提交,即自动提交和非自动提交。
自动提交事务
默认一个 SQL 语句为一个事务,当此语句执行成功后,由系统隐式提交。数据更改被数据库接受。当 SQL 执行失败,自然视做回滚,不对数据造成影响。 KingbaseES 默认提交事务方式为自动提交。
非自动提交事务
事务的开始为第一个 SQL 语句。事务的结束必须用户显式的使用 COMMIT 语句提交整个事务或者使用 ROLLBACK 语句回滚整个事务。如果客户端连接断开,那么未提交的事务做回滚操作。 当用户想自己控制事务的执行,可以显式的用 BEGIN 语句开始一个事务,然后在事务结束时应该使用 COMMIT 或 ROLLBACK 语句显式提交。
接口中也有对事务提交方式的控制,配置相应的配置文件可以使会话处于自动提交或非自动提交模式。
一个事务的提交和回滚对数据库实例造成的影响为:
一个事务提交对数据库实例造成如下影响:
所有数据库更改对整个数据库生效。
保证此事务产生的日志写到磁盘。
释放所有此事务进行过程中申请的锁。
数据库标记此事务结束。
一个事务回滚对数据库实例造成如下影响:
撤销所有对数据的更改到此事务开始之前的状态。
释放所有此事务进行过程中申请的锁。
数据库标记此事务结束。
4. 并发控制
KingbaseES为开发者提供了一组丰富的工具来管理对数据的并发访问。在内部,数据一致性通过使用一种多版本模型(多版本并发控制,MVCC)来维护。这就意味着每个SQL语句看到的都只是一小段时间之前的数据快照(一个数据库版本),而不管底层数据的当前状态。这样可以保护语句不会看到可能由其他在相同数据行上执行更新的并发事务造成的不一致数据,为每一个数据库会话提供事务隔离。MVCC避免了传统的数据库系统实现并发控制使用的封锁方法,将锁争夺最小化来允许多用户环境中的合理性能。
使用MVCC而不是封锁来实现并发控制的主要优点是,MVCC中对查询(读)数据的锁请求与写数据的锁请求不冲突,所以读不会阻塞写,而写也不阻塞读。甚至在使用由可序列化快照隔离(SSI)技术实现的最严格的可序列化事务隔离级别时,KingbaseES也能保持读写不冲突。
在KingbaseES里也有表和行级别的锁功能,用于那些通常不需要完整事务隔离并且想要显式管理特定冲突点的应用。不过,恰当地使用MVCC通常会提供比锁更好的性能。另外,KingbaseES也提供咨询锁实现由应用定义语义的封锁,这种封锁机制可以在事务或session级别实现不与数据库本身封锁机制冲突的封锁。
4.1. 事务隔离
SQL标准定义了四种隔离级别。最严格的是可序列化,定义是:一组可序列化事务的任意并发执行被保证效果和以某种顺序一个一个执行这些事务一样。其他三种级别使用并发事务之间交互产生的现象来定义,每一个级别中都要求必须不出现一种现象。注意由于可序列化的定义,在该级别上这些现象都不可能发生。
在各个级别上被禁止出现的现象是:
脏读
一个事务读取了另一个并行未提交事务写入的数据。
不可重复读
一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提交)修改。
幻读
一个事务重新执行一个返回符合一个搜索条件的行集合的查询,发现满足条件的行集合因为另一个最近提交的事务而发生了改变。
序列化异常
成功提交一组事务的结果与这些事务所有可能的串行执行结果都不一致。
SQL 标准和 KingbaseES 实现的事务隔离级别在下表事务隔离级别中描述。
隔离级别 |
脏读 |
不可重复读 |
幻读 |
序列化异常 |
---|---|---|---|---|
读未提交 |
可能,但在 KES中不可能 |
可能 |
可能 |
可能 |
读已提交 |
不可能 |
可能 |
可能 |
可能 |
可重复读 |
不可能 |
不可能 |
可能,但在 KES中不可能 |
可能 |
可序列化 |
不可能 |
不可能 |
不可能 |
不可能 |
在KingbaseES中,可以使用四种标准事务隔离级别中的任意一种,但是内部只实现了三种不同的隔离级别,KingbaseES的读未提交级别的行为和读已提交相同。这是把标准隔离级别映射到KingbaseES的多版本并发控制架构的唯一合理的方法。
表格中也表明了KingbaseES的可重复读实现不允许幻读。SQL标准允许这种更严格的定义:四种隔离级别只定义了哪种现像不能发生,但是没有定义哪种现像必须发生。可用的隔离级别的行为将在下面的小节中详细描述。
要设置一个事务的事务隔离级别,使用SET TRANSACTION命令。
注意
某些KingbaseES数据类型和函数关于事务的行为有特殊的规则。特别是,对一个序列的修改(以及用serial声明的一列的计数器)是立刻对所有其他事务可见的,并且在作出该修改的事务中断时也不会被回滚。
-
读已提交隔离级别
-
可重复读隔离级别
-
可序列化隔离级别
4.1.1. 读已提交隔离级别
读已提交是KingbaseES中的默认隔离级别。 当一个事务运行使用这个隔离级别时, 一个查询(没有FOR UPDATE/SHARE子句)只能看到查询开始之前已经被提交的数据,而无法看到未提交的数据或在查询执行期间其它事务提交的数据。实际上,SELECT查询看到的是一个在查询开始运行的瞬间该数据库的一个快照。不过SELECT可以看见在它自身事务中之前执行的更新的效果,即使它们还没有被提交。还要注意的是,即使在同一个事务里两个相邻的SELECT命令可能看到不同的数据,因为其它事务可能会在第一个SELECT开始和第二个SELECT开始之间提交。
UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命令在搜索目标行时的行为和SELECT一样:它们将只找到在命令开始时已经被提交的行。 不过,在被找到时,这样的目标行可能已经被其它并发事务更新(或删除或锁住)。在这种情况下,即将进行的更新将等待第一个更新事务提交或者回滚(如果它还在进行中)。如果第一个更新事务回滚,那么它的作用将被忽略并且第二个事务可以继续更新最初发现的行。如果第一个更新事务提交,若该行被第一个更新者删除,则第二个更新事务将忽略该行,否则第二个更新者将试图在该行的已被更新的版本上应用它的操作。该命令的搜索条件(WHERE子句)将被重新计算来看该行被更新的版本是否仍然符合搜索条件。如果符合,则第二个更新者使用该行的已更新版本继续其操作。在SELECT FOR UPDATE和SELECT FOR SHARE的情况下,这意味着把该行的已更新版本锁住并返回给客户端。
带有ON CONFLICT DO UPDATE子句的 INSERT行为类似。在读已提交模式,要插入的每一行将被插入或者更新。除非有其他错误出现,这两种结果之一一定会发生。如果在另一个事务中发生了冲突的修改,并且其效果对于INSERT还不可见,则UPDATE子句将会影响那行数据,即便那一行对于该命令来说还没有可见版本。
带有ON CONFLICT DO NOTHING子句的 INSERT有可能因为另一个效果对INSERT快照不可见的事务的结果无法让插入进行下去。再次强调,这只是读已提交模式中的情况。
因为上面的规则,正在更新的命令可能会看到一个不一致的快照:它们可以看到并发更新命令在它尝试更新的相同行上的效果,但是却看不到那些命令对数据库里其它行的效果。这样的行为令读已提交模式不适合用于涉及复杂搜索条件的命令。不过,它对于更简单的情况是正确的。例如可以考虑用这样的命令更新银行余额:
BEGIN; UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345; UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534; COMMIT;
如果两个这样的事务同时尝试修改帐号 12345 的余额,那我们很明显希望第二个事务从账户行的已更新版本上开始工作。 因为每个命令只影响一个已经决定了的行,让它看到行的已更新版本不会导致任何麻烦的不一致性。
在读已提交模式中,更复杂的使用可能产生不符合需要的结果。例如:考虑一个在数据上操作的DELETE命令,它操作的数据正被另一个命令从它的限制条件中移除或者加入,例如,假定website是一个两行的表,两行的website.hits等于9和10:
BEGIN; UPDATE website SET hits = hits + 1; -- run from another session: DELETE FROM website WHERE hits = 10; COMMIT;
即便在UPDATE之前有一个website.hits = 10的行,DELETE将不会产生效果。这是因为更新之前的行值9被跳过,并且当UPDATE完成并且DELETE获得一个锁,新行值不再是10而是11,这再也不匹配条件了。
因为在读已提交模式中,每个命令都是从一个新的快照开始的,而这个快照包含在该时刻已提交的事务,因此同一事务中的后续命令将看到任何已提交的并行事务的效果。以上的焦点在于单个命令是否看到数据库的绝对一致的视图。
读已提交模式提供的部分事务隔离对于许多应用而言是足够的,并且这个模式速度快并且使用简单。不过,它不是对于所有情况都够用。做复杂查询和更新的应用可能需要比读已提交模式提供的更严格一致的数据库视图。
4.1.2. 可重复读隔离级别
可重复读隔离级别只看到在事务开始之前被提交的数据;它从来看不到未提交的数据或者并行事务在本事务执行期间提交的修改(不过,查询能够看见在它的事务中之前执行的更新,即使它们还没有被提交)。这是比SQL标准对此隔离级别所要求的更强的保证,并且阻止表事务隔离级别中描述的除了序列化异常之外的所有现象。如上面所提到的,这是标准特别允许的,标准只描述了每种隔离级别必须提供的最小保护。
这个级别与读已提交不同之处在于,一个可重复读事务中的查询可以看见在事务中第一个非事务控制语句开始时的一个快照,而不是事务中当前语句开始时的快照。因此,在一个单一事务中的后续SELECT命令看到的是相同的数据,即它们看不到其他事务在本事务启动后提交的修改。
使用这个级别的应用必须准备好由于序列化失败而重试事务。
UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命令在搜索目标行时的行为和SELECT一样:它们将只找到在事务开始时已经被提交的行。不过,在被找到时,这样的目标行可能已经被其它并发事务更新(或删除或锁住)。在这种情况下,可重复读事务将等待第一个更新事务提交或者回滚(如果它还在进行中)。如果第一个更新事务回滚,那么它的作用将被忽略并且可重复读事务可以继续更新最初发现的行。但是如果第一个更新事务提交(并且实际更新或删除该行,而不是只锁住它),则可重复读事务将回滚并带有如下消息
ERROR: could not serialize access due to concurrent update
因为一个可重复读事务无法修改或者锁住被其他在可重复读事务开始之后的事务改变的行。
当一个应用接收到这个错误消息,它应该中断当前事务并且从开头重试整个事务。在第二次执行中,该事务将见到作为其初始数据库视图一部分的之前提交的改变,这样在使用行的新版本作为新事务更新的起点时就不会有逻辑冲突。
注意只有更新事务可能需要被重试;只读事务将永远不会有序列化冲突。
可重复读模式提供了一种严格的保证,在其中每一个事务看到数据库的一个完全稳定的视图。不过,这个视图并不需要总是和同一级别上并发事务的某些序列化(一次一个)执行保持一致。例如,即使这个级别上的一个只读事务可能看到一个控制记录被更新,这显示一个批处理已经被完成但是不能看见作为该批处理的逻辑组成部分的一个细节记录,因为它读取空值记录的一个较早的版本。如果不小心地使用显式锁来阻塞冲突事务,尝试用运行在这个隔离级别的事务来强制业务规则不太可能正确地工作。
注意
在KingbaseES V8.3 版本之前,一个对于可序列化事务隔离级别的请求会提供和这里描述的完全一样的行为。为了保持之前版本的可序列化行为,现在应该请求可重复读级别。
4.1.3. 可序列化隔离级别
可序列化隔离级别提供了最严格的事务隔离。这个级别为所有已提交事务模拟序列事务执行;就好像事务被按照序列一个接着另一个被执行,而不是并行地被执行。但是,和可重复读级别相似,使用这个级别的应用必须准备好因为序列化失败而重试事务。事实上,这个隔离级别完全像可重复读一样地工作,除了它会监视一些条件,这些条件可能导致一个可序列化事务的并发集合的执行产生的行为与这些事务所有可能的序列化(一次一个)执行不一致。这种监控不会引入超出可重复读之外的阻塞,但是监控会产生一些负荷,并且对那些可能导致序列化异常的条件的检测可能触发一次序列化失败的错误。
例如,考虑一个表mytab,它初始时包含:
class | value ------+------- 1 | 10 1 | 20 2 | 100 2 | 200
假设可序列化事务 A 计算:
SELECT SUM(value) FROM mytab WHERE class = 1;
并且接着把结果(3)作为一个新行的value插入,新行的class = 2。同时,可序列化事务 B 计算:
SELECT SUM(value) FROM mytab WHERE class = 2;
并得到结果 300,它会将其与class = 1插入到一个新行中。然后两个事务都尝试提交。如果其中一个事务运行在可重复读隔离级别,两者都被允许提交;但是由于没有执行的序列化顺序能在结果上一致,使用可序列化事务将允许一个事务提交并且将回滚另一个并伴有这个消息:
ERROR: could not serialize access due to read/write dependencies among transactions
这是因为,如果 A 在 B 之前执行,B 将计算得到合计值 330 而不是300,而且相似地另一种顺序将导致 A 计算出一个不同的合计值。
当依赖可序列化事务来阻止异常时,重要的一点是任何从一个持久化用户表读出数据都不被认为是有效的,直到读它的事务已经成功提交为止。即便是对只读事务也是如此,除了在一个可推迟的只读事务中读取的数据是读出以后立刻有效的,因为这样的一个事务在开始读取任何数据之前会等待,直到它能获得一个快照保证来避免这种问题为止。在所有其他情况下,应用不能依靠在一个后来被中断的事务中读取的结果;相反,它们应当重试事务直到它成功。
KingbaseES使用谓词锁保证真正的可序列化,系统会保存这些锁以便能够判断什么时候一个写操作会对一个并发事务中先发生的读取结果产生影响。在KingbaseES中,这些锁并不导致任何阻塞,因此不会导致任何死锁。它们被用来标识并发可序列化事务之间的依赖性,这些事务的组合可能导致序列化异常。作为对比,在读已提交或可重复读级别事务如果想要保证数据一致性,可能需要持有表级的锁,这可能阻塞其他尝试使用该表的用户;或者通过SELECT FOR UPDATE或SELECT FOR SHARE实现,在可能阻塞其他事务的同时还会导致磁盘I/O。
像大部分其他数据库系统,KingbaseES中的谓词锁基于被一个事务真正访问的数据。这些谓词锁将显示在sys_locks系统视图中,它们的mode为SIReadLock。这种在一个查询执行期间获得的特别的锁将依赖于该查询所使用的计划,并且在事务过程中多个细粒度锁(如元组锁)可能和少量粗粒度锁(如页面锁)相结合来防止耗尽用于跟踪锁的内存。如果一个READ ONLY事务检测到不会有导致序列化异常的冲突发生,它可以在完成前释放其SIRead 锁。事实上,READ ONLY事务将常常可以在启动时确立这一事实并避免拿到任何谓词锁。如果你显式地请求一个SERIALIZABLE READ ONLY DEFERRABLE事务,它将阻塞直到它能够确立这一事实(这是唯一一种可序列化事务阻塞但可重复读事务不阻塞的情况)。在另一方面,SIRead 锁常常需要被保持到事务提交之后,直到重叠的读写事务完成。
坚持使用可序列化事务可以简化开发。成功提交的并发可序列化事务的任意集合将得到和一次运行一个相同效果的这种保证意味着,如果你能证明一个单一事务在独自运行时能做正确的事情,则你可以相信它在任何混合的可序列化事务中也能做正确的事情,即使它不知道那些其他事务做了些什么,否则它将不会成功提交。重要的是使用这种技术的环境有一种普遍的方法来处理序列化失败(总是会返回一个 SQLSTATE 值'40001'),因为它将很难准确地预计哪些事务可能为读/写依赖性做贡献并且需要被回滚来阻止序列化异常。读/写依赖性的监控会产生开销,如重启被序列化失败中止的事务,但是作为在该开销和显式锁及SELECT FOR UPDATE或SELECT FOR SHARE导致的阻塞之间的一种平衡,可序列化事务是在某些环境中最好性能的选择。
虽然KingbaseES的可序列化事务隔离级别只允许并发事务在能够证明有一种串行执行能够产生相同效果的前提下提交,但它却不能总是阻止在真正的串行执行中不会发生的错误产生。尤其是可能会看到由于可序列化事务重叠执行导致的唯一约束被违背的情况,这些情况即便在尝试插入键之前就显式地检查过该键不存在也会发生。避免这种问题的方法是,确保所有插入可能会冲突的键的可序列化事务首先显式地检查它们能不能那样做。例如,试想一个要求用户输入新键的应用,它会通过尝试查询用户给出的键来检查键是否已经存在,或者是通过选取现有最大的键并且加一来产生一个新键。如果某些可序列化事务不遵循这种协议而直接插入新键,则也可能会报告唯一约束被违背,即便在并发事务串行执行的情况下不会发生唯一约束被违背也是如此。
当依赖可序列化事务进行并发控制时,为了最佳性能应该考虑一下问题:
在可能时声明事务为READ ONLY。
控制活动连接的数量,如果需要使用一个连接池。这总是一个重要的性能考虑,但是在一个使用可序列化事务的繁忙系统中这尤为重要。
只在一个单一事务中放完整性目的所需要的东西。
不要让连接不必要地处于“idle in transaction”状态。配置参数idle_in_transaction_session_timeout可以被用来自动断开此类连接。
在那些由于使用可序列化事务自动提供的保护的地方消除不再需要的显式锁、SELECT FOR UPDATE和SELECT FOR SHARE。
当系统因为谓词锁表内存短缺而被强制结合多个页面级谓词锁为一个单一的关系级谓词锁时,序列化失败的比例可能会上升。你可以通过增加max_pred_locks_per_transaction、max_pred_locks_per_relation和max_pred_locks_per_page来避免这种情况。
一次顺序扫描将总是需要一个关系级谓词锁。这可能导致序列化失败的比例上升。通过缩减random_page_cost和/或增加cpu_tuple_cost来使优化器倾向使用索引扫描将有助于缓解此类问题。
4.2. 显式锁定
KingbaseES提供了多种锁模式用于控制对表中数据的并发访问。这些模式可以用于在MVCC无法给出期望行为的情境中由应用控制的锁。同样,大多数KingbaseES命令会自动要求恰当的锁以保证被引用的表在命令的执行过程中不会以一种不兼容的方式删除或修改(例如,TRUNCATE无法安全地与同一表中上的其他操作并发地执行,因此它在表上获得一个排他锁来保障这个行为)。
要检查在一个数据库服务器中当前未解除的锁列表,可以使用sys_locks系统视图。有关监控锁管理器子系统状态的更多信息,请参考监控数据库活动。
-
表级锁
-
行级锁
-
页级锁
-
死锁
-
咨询锁
4.2.1. 表级锁
下面的列表显示了可用的锁模式和KingbaseES自动使用它们的场合。也可以用LOCK命令显式获得这些锁。请记住所有这些锁模式都是表级锁,即使它们的名字包含“row”单词(这些名称是历史遗留问题)。在一定程度上,这些名字反应了每种锁模式的典型用法 — 但是语意却都是一样的。 两种锁模式之间真正的区别是它们有着不同的冲突锁模式集合。两个事务在同一时刻不能在同一个表上持有属于相互冲突模式的锁(但是,一个事务决不会和自身冲突。例如,它可以在同一个表上获得ACCESS EXCLUSIVE锁然后接着获取ACCESS SHARE锁)。非冲突锁模式可以由许多事务同时持有。 请特别注意有些锁模式是自冲突的(例如,在一个时刻ACCESS EXCLUSIVE锁不能被多于一个事务持有)而其他锁模式不是自冲突的(例如,ACCESS SHARE锁可以被多个事务持有)。
表级锁模式
ACCESS SHARE
只与ACCESS EXCLUSIVE锁模式冲突。 SELECT命令在被引用的表上获得一个这种模式的锁。通常,任何只读取表而不修改它的查询都将获得这种锁模式。
ROW SHARE
与EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。 SELECT FOR UPDATE和SELECT FOR SHARE命令在目标表上取得一个这种模式的锁(加上在被引用但没有选择FOR UPDATE/FOR SHARE的任何其他表上的ACCESS SHARE锁)。
ROW EXCLUSIVE
与SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。 命令UPDATE、DELETE和INSERT在目标表上取得这种锁模式(加上在任何其他被引用表上的ACCESS SHARE锁)。通常,这种锁模式将被任何修改表中数据的命令取得。
SHARE UPDATE EXCLUSIVE
与SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发模式改变和VACUUM运行的影响。 由VACUUM(不带FULL)、 ANALYZE、CREATE INDEX CONCURRENTLY、 REINDEX CONCURRENTLY, CREATE STATISTICS,以及 ALTER INDEX和ALTER TABLE的变体获得(更多信息,参见 ALTER INDEX and ALTER TABLE)。
SHARE
与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发数据改变的影响。 由CREATE INDEX(不带CONCURRENTLY)取得。
SHARE ROW EXCLUSIVE
与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式保护一个表不受并发数据修改所影响,并且是自排他的,这样在一个时刻只能有一个会话持有它。 由CREATE TRIGGER和很多 ALTER TABLE的很多形式所获得(见 ALTER TABLE)。
EXCLUSIVE
与ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。这种模式只允许并发的ACCESS SHARE锁,即只有来自于表的读操作可以与一个持有该锁模式的事务并行处理。 由REFRESH MATERIALIZED VIEW CONCURRENTLY获得。
ACCESS EXCLUSIVE
与所有模式的锁冲突(ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE)。这种模式保证持有者是访问该表的唯一事务。 由ALTER TABLE、DROP TABLE、 TRUNCATE、REINDEX、 CLUSTER、VACUUM FULL和REFRESH MATERIALIZED VIEW(不带CONCURRENTLY) 命令获取。ALTER INDEX和ALTER TABLE的很多形式也在这个层面上获得锁 (见ALTER TABLE)。这也是未显式指定模式的 LOCK TABLE命令的默认锁模式。
注意
只有ACCESS EXCLUSIVE锁阻塞SELECT(不带FOR UPDATE/SHARE)语句。
一旦被获取,一个锁通常将被持有直到事务结束。但是如果在建立保存点之后才获得锁,那么在回滚到这个保存点的时候将立即释放该锁。这与ROLLBACK取消保存点之后所有的影响的原则保持一致。同样的原则也适用于在PL/SQL异常块中获得的锁:一个跳出块的错误将释放在块中获得的锁。
表 2 冲突的锁模式 请求的锁模式
当前的锁模式
ACCESS SHARE
ROW SHARE
ROW EXCLUSIVE
SHARE UPDATE EXCLUSIVE
SHARE
SHARE ROW EXCLUSIVE
EXCLUSIVE
ACCESS EXCLUSIVE
ACCESS SHARE
X
ROW SHARE
X
X
ROW EXCLUSIVE
X
X
X
X
SHARE UPDATE EXCLUSIVE
X
X
X
X
X
SHARE
X
X
X
X
X
SHARE ROW EXCLUSIVE
X
X
X
X
X
X
EXCL USIVE
X
X
X
X
X
X
X
ACCESS EXCLUSIVE
X
X
X
X
X
X
X
X
4.2.2. 行级锁
除了表级锁以外,KingbaseES中还有行级锁,下文列出了行级锁以及在哪些情境下KingbaseES会自动使用它们。行级锁的完整冲突表请见表"冲突的行级锁"。注意同一个事务可能会在相同的行上持有冲突的锁,甚至是在不同的子事务中。但是除此之外,两个事务永远不可能在相同的行上持有冲突的锁。行级锁不影响数据查询,它们只阻塞对同一行的写入者和加锁者。
行级锁模式
FOR UPDATE
FOR UPDATE会导致由SELECT语句检索到的行被锁定,就好像它们要被更新。这可以阻止它们被其他事务锁定、修改或者删除,一直到当前事务结束。也就是说其他尝试UPDATE、DELETE、SELECT FOR UPDATE、SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE这些行的事务将被阻塞,直到当前事务结束。反过来,SELECT FOR UPDATE将等待已经在相同行上运行以上这些命令的并发事务,并且接着锁定并且返回被更新的行(或者没有行,因为行可能已被删除)。不过,在一个REPEATABLE READ或SERIALIZABLE事务中,如果一个要被锁定的行在事务开始后被更改将会抛出一个错误。 任何在一行上的DELETE命令也会获得FOR UPDATE锁模式,在某些列上修改值的UPDATE也会获得该锁模式。当前UPDATE情况中被考虑的列集合是那些具有能用于外键的唯一索引的列(所以部分索引和表达式索引不被考虑),但是这种要求未来有可能会改变。
FOR NO KEY UPDATE
行为与FOR UPDATE类似,不过获得的锁较弱:这种锁将不会阻塞尝试在相同行上获得锁的SELECT FOR KEY SHARE命令。任何不获取FOR UPDATE锁的UPDATE也会获得这种锁模式。
FOR SHARE
行为与FOR NO KEY UPDATE类似,不过它在每个检索到的行上获得一个共享锁而不是排他锁。一个共享锁会阻塞其他事务在这些行上执行UPDATE、DELETE、SELECT FOR UPDATE或者SELECT FOR NO KEY UPDATE,但是它不会阻止它们执行SELECT FOR SHARE或者SELECT FOR KEY SHARE。
FOR KEY SHARE
行为与FOR SHARE类似,不过锁较弱:SELECT FOR UPDATE会被阻塞,但是SELECT FOR NO KEY UPDATE不会被阻塞。一个键共享锁会阻塞其他事务执行修改键值的DELETE或者UPDATE,但不会阻塞其他UPDATE,也不会阻止SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE。
KingbaseES不会在内存里保存任何关于已修改行的信息,因此对一次锁定的行数没有限制。但是,锁住一行会导致一次磁盘写,例如,SELECT FOR UPDATE将修改选中的行以标记它们被锁住,并且因此会导致磁盘写入。
请求的锁模式 |
当前的锁模式 |
|||
---|---|---|---|---|
FOR KEY SHARE |
FOR SHARE |
FOR NO KEY UPDATE |
FOR UPDATE |
|
FOR KEY SHARE |
X |
|||
FOR SHARE |
X |
X |
||
FOR NO KEY UPDATE |
X |
X |
X |
|
FOR UPDATE |
X |
X |
X |
X |
4.2.3. 页级锁
除了表级别和行级别的锁以外,页面级别的共享/排他锁被用来控制对共享缓冲池中表页面的读/写。这些锁在行被获取或者更新后马上被释放。应用开发者通常不需要关心页级锁,在这里提到它们只是为了完整性。
4.2.4. 死锁
显式锁定的使用可能会增加死锁的可能性,死锁是指两个(或多个)事务相互持有对方想要的锁。例如,如果事务1在表 A上获得一个排他锁,同时试图获取一个在表 B 上的排他锁, 而事务2已经持有表 B 的排他锁,同时却正在请求表 A 上的一个排他锁,那么两个事务就都不能进行下去。KingbaseES能够自动检测到死锁情况并且会通过中断其中一个事务从而允许其它事务完成来解决这个问题(具体哪个事务会被中断是很难预测的,而且也不应该依靠这样的预测)。
要注意死锁也可能会作为行级锁的结果而发生(并且因此,它们即使在没有使用显式锁定的情况下也会发生)。考虑如下情况,两个并发事务在修改一个表。第一个事务执行:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;
这样就在指定帐号的行上获得了一个行级锁。然后,第二个事务执行:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222; UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;
第一个UPDATE语句成功地在指定行上获得了一个行级锁,因此它成功更新了该行。 但是第二个UPDATE语句发现它试图更新的行已经被锁住了,因此它等待持有该锁的事务结束。事务二现在就在等待事务一结束,然后再继续执行。现在,事务一执行:
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;
事务一试图在指定行上获得一个行级锁,但是它得不到:事务二已经持有了这样的锁。所以它要等待事务二完成。因此,事务一被事务二阻塞,而事务二也被事务一阻塞:一个死锁。 KingbaseES将检测这样的情况并中断其中一个事务。
防止死锁的最好方法通常是保证所有使用一个数据库的应用都以一致的顺序在多个对象上获得锁。在上面的例子里,如果两个事务以同样的顺序更新那些行,那么就不会发生死锁。 我们也应该保证一个事务中在一个对象上获得的第一个锁是该对象需要的最严格的锁模式。如果我们无法提前做到这些,那么可以通过重试因死锁而中断的事务来即时处理死锁。
只要没有检测到死锁情况,请求一个表级或行级锁的事务将无限等待冲突锁被释放。这意味着一个应用长时间保持事务开启不是什么好事(例如等待用户输入)。
4.2.5. 咨询锁
KingbaseES提供了一种方法创建由应用定义其语义的锁。这种锁被称为咨询锁,因为系统并不强迫其使用 — 而是由应用来保证其正确的使用。咨询锁可用于 MVCC 模型不适用的锁定策略。例如,咨询锁的一种常用用法是模拟悲观锁策略。虽然一个存储在表中的标志可以被用于相同目的,但咨询锁更快、可以避免表膨胀并且会由服务器在会话结束时自动清理。
有两种方法在KingbaseES中获取一个咨询锁:在会话级别或在事务级别。一旦在会话级别获得了咨询锁,它将被保持直到被显式释放或会话结束。不同于标准锁请求,会话级咨询锁请求不保持事务语义:在一个后来被回滚的事务中得到的锁在回滚后仍然被保持。一个锁在它所属的进程中可以被获取多次;对于每一个完成的锁请求必须有一个相应的解锁请求,直至锁被真正释放。在另一方面,事务级锁请求的行为更像普通锁请求:在事务结束时会自动释放它们,不需要显式的解锁操作。这种行为通常比会话级别的行为更方便,因为它使用一个咨询锁的时间更短。对于同一咨询锁标识符的会话级别和事务级别的锁请求按照期望将彼此阻塞。如果一个会话已经持有了一个给定的咨询锁,由它发出的对这个锁的请求将总是成功,即使有其他会话在等待该锁;不管现有的锁和新请求是处在会话级别还是事务级别,这种说法都是成立的。
和所有KingbaseES中的锁一样,当前被任何会话所持有的咨询锁的完整列表可以在sys_locks系统视图中找到。
咨询锁和普通锁都被存储在一个共享内存池中,它的尺寸由max_locks_per_transaction和max_connections配置变量定义。 需要注意不要耗尽这些内存,否则服务器将不能再授予任何锁。这对服务器可以授予的咨询锁数量设置了一个上限,根据服务器的配置不同,这个限制通常是数万到数十万。
在使用咨询锁方法的特定情况下,特别是查询中涉及显式排序和LIMIT子句时,由于 SQL 表达式被计算的顺序,必须小心控制锁的获取。例如:
SELECT sys_advisory_lock(id) FROM foo WHERE id = 12345; -- ok SELECT sys_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- danger! SELECT sys_advisory_lock(q.id) FROM ( SELECT id FROM foo WHERE id > 12345 LIMIT 100 ) q; -- ok
在上述查询中,第二种形式是危险的,因为不能保证在锁定函数被执行之前应用LIMIT。这可能导致获得某些应用不期望的锁,并因此在会话结束之前无法释放。 从应用的角度来看,这样的锁将被挂起,虽然它们仍然在sys_locks中可见。
4.2.6. 锁定和索引
尽管KingbaseES提供对表数据访问的非阻塞读/写,但并非KingbaseES中实现的每一个索引访问方法当前都能够提供非阻塞读/写访问。不同的索引类型按照下面方法操作:
B-tree、GiST和SP-GiST索引
短期的页面级共享/排他锁被用于读/写访问。每个索引行被取得或被插入后立即释放锁。这些索引类型提供了无死锁情况的最高并发性。
Hash索引
Hash 桶级别的共享/排他锁被用于读/写访问。锁在整个 Hash 桶处理完成后释放。Hash 桶级锁比索引级的锁提供了更好的并发性但是可能产生死锁,因为锁持有的时间比一次索引操作时间长。
GIN索引
短期的页面级共享/排他锁被用于读/写访问。锁在索引行被插入/抓取后立即释放。但要注意的是一个 GIN 索引值的插入通常导致对每行产生几个索引键的插入,因此 GIN 可能为了插入一个单一值而做大量的工作。 目前,B-tree 索引为并发应用提供了最好的性能。因为它还有比 Hash 索引更多的特性,在那些需要对标量数据进行索引的并发应用中,我们建议使用 B-tree 索引类型。在处理非标量类型数据的时候,B-tree 就没什么用了,应该使用 GiST、SP-GiST 或 GIN 索引替代。