9 数据并发和一致性
主要内容包括
数据并发和一致性的简介
事务隔离级别
数据库锁机制
锁的自动实现
手工管理锁
用户定义的锁
9.1 数据并发和一致性的简介多用户并发的数据库环境必须符合如下要求
1,数据并发:用户可以同时从数据库获取数据
2,数据一致性:每个用户会看到数据的一致性视图,包括自己事务内的修改的数据和其它用户已经提交的事务修改
为了描述并发时的事务的一致性,数据库研究人员定义了一个叫做串行化(serializability)的事务隔离模型。在数据库中,串行的事务操作可以确保看起来没有其它用户在修改数据
串行化的事务会影响高并发的数据库性能,串行的完全隔离意味着当一个事务查询表中数据时,不允许其它事务插入数据。简单来说,现实世界中,需要在事务隔离和性能之间取得平衡。
Oracle数据库使用多版本的一致性模型,锁机制和事务实现数据一致性。通过这种方法,数据库能提供给所有并发用户在某个时间点的一致性视图。由于同时存在多个版本的数据块,事务能够提供查询所需的某个时间点的数据版本
Oracle中,多版本(multiversioning)是同时提供多个数据版本,oracle提供多个版本的数据一致性,这意味着数据库查询具有如下特性
读一致性查询
查询出来的数据是一个时间点的一致视图
重要:oracle不允许脏读(dirty reads),脏读是指视图读取到了另一个事务未提交的数据。脏读会带来如下类似的问题,假设一个事务更新了一列的数值,且没有提交,第二个事务读到了这个更新的脏数据,第一个事务回滚了相关数据,而第二个事务继续处理了更新的数据,这就会给数据库带来很大的破坏。脏读违反了数据一致性、和外键约束等
非阻塞查询
读操作和写操作不会相互阻塞
oracle确保语句级别的数据一致性,能够保证查询返回某个时间点的所有提交的一致性数据。一致性的时间点取决于事务隔离级别和sql的内容
1,在提交读隔离级别(read committed isolation level),返回数据的时间点就是查询开始的时间。
2,在串行和只读事务(serializable or read-only transaction),数据一致性的时间点是事务开始的时间
3,闪回查询(Flashback Query)中(SELECT ... AS OF),select语句明确制定了一致性时间点
oracle也提供事务级别的读一致性,事务内的所有查询都能够查询到事务开始时间点的一致性数据。本事务内修改的数据,事务内查询可以看到。例如事务修改了employees表,接下来的查询会看到这个更新。事务级别的读一致性不会带来重复读(repeatablereads)问题,不会带来幻影读(phantom reads)
为了多版本读一致性模型,当表被并发查询和更新时,数据库需要创建一系列的读一致性数据,oracle是通过undo来实现的。当用户修改数据时,oracle往undo段中写入undo实体(undo entries),undo段中存储没提交的事务或者最近已提交事务中的被修改的旧数据。数据库存在对同一个数据,在不同时间点的多个版本,数据库能够根据不同时间点的数据快照,提供给查询读一性视图,而不用阻塞查询。在单实例和rac环境下,都能保证读一致性。rac在不同实例之间使用cache-to-cache数据块传输机制,也就是cache fushion的方式传输数据块的读一致性镜像
图9-1是通过undo在read committed隔离级别实现语句级别的读一致性
Figure 9-1 Read Consistency in the ReadCommitted Isolation Level
图:提交读隔离级别中的数据一致性
当查询需要读取数据块时,如果数据块被修改,数据库能够通过回滚改变的方式,重构数据块中的数据为查询开始时间点的数据。数据库使用SCN(system change number)来保证事务的顺序,当查询开始时,数据库记录此时的scn,例如图标中scn是10023,那么查询只能看到scn时和之前提交的数据。如图9-1,如果scn 10024修改了两个数据块的数据,而查询到这两个数据块时,数据库拷贝当前数据块到新内存中,并应用undo数据重建为之前的数据块版本,这种数据库重建操作叫做一致性读(CR)克隆consistent read (CR) clones.在表9-1中,数据库生成了2个CR clones:一个对应SCN10006,一个是SCN 10021,数据库返回重建后的数据给查询,通过这种方式,数据库避免了脏读(dirty
数据块的块头(block header)中,有事务表ITL(interestedtransaction list (ITL)),当数据库修改数据块时,ITL判断决定事务是否提交。ITL用来描述锁定数据块中某些行的事务,这些行包含提交和未提交的修改。ITL指向UNDO段中的事务表,事务表提供了数据改变的时间。也就是说,数据块头包含最近修改数据块的事务历史信息。表的INITRANS属性控制能够保持的事务的数量。
一般来说,数据库使用数据加锁的方式来解决数据并发、一致性和完整性。锁是不同事物获取同一个资源时的一种排他机制
ANSI和ISO组织的SQL标准,定义了四种事物隔离级别,不同的隔离级别对事物的处理有不同程度的影响,这些隔离级别定义了需要被避免三种现象
脏读(Dirty reads):一个事物读到了另一个事物未提交的数据
非重复读(Nonrepeatable (fuzzy)reads):一个事物重新读之前的数据,发现另一个提交的事物已经修改或者删除了相关数据。例如,一个用户查询了某一行,后来又查询这一行时,发现数据被别的事物修改了
幻影读(Phantom reads):一个事物执行了一个满足一定条件的查询,发现另一个提交的事物插入了满足条件的的某些数据
例如:一个事物查询了employees表中的某些行,五分钟后,重新执行相同的查询,发现返回的行数增加了,原因是其它用户插入了符合条件的数据,和不可重复读的区别是读到的数据没被修改,而是增加了某些行。
Isolation Level |
Dirty Read |
Nonrepeatable Read |
Phantom Read |
Read uncommitted |
Possible |
Possible |
Possible |
Read committed |
Not possible |
Possible |
Possible |
Repeatable read |
Not possible |
Not possible |
Possible |
Serializable |
Not possible |
Not possible |
Not possible |
sql标准组织定义了4种隔离级别,事物运行在不同隔离级别具有不同的特性
oracle提供了read committed (默认)和串行(serializable)隔离级别,另外,还提供了只读模式
ANSI标准定义了不同的隔离级别,提交读隔离,串行隔离和只读隔离级别
提交读是默认的隔离级别,事务中执行的查询只能查询开始之前就已经提交的数据,而不是事物开始时间点前提交的。这种隔离级别适合事物之间冲突较少的数据库中。
查询开始之后才提交的数据不会被查询到,例如,一个查询在扫描一百万行的一张表,另一个事物修改了其中的950,000行并提交,当前的查询看不到这些修改,然而,由于数据库不防止其他事务修改数据,其它事务可能在两次查询之间修改相关数据,当下次运行同一个查询时,可能会导致查询到的数据不一致,这就可能导致非重复读和幻影读问题。也就是说提交读隔离级别不会导致脏读,但会导致非重复读和幻影读。
每一个查询都能保证一致性结果,而不需要用户干预。一个隐含的查询,例如update语句中的where条件,也能保证数据一致性。隐含查询看不到DML本身的处理导致的数据改变,而是看到语句开始之前的数据。
如果select语句中包含PL/SQL函数,pl/sql中的语句也会在pl/sql函数内执行语句级别的读一致性,而不是父sql级别。例如,一个函数可以查询到表中另一个用户改变和提交的数据,函数中的每个select执行,都需要新建一个提交读的快照,遵守语句级别的提交读隔离级别
提交读事物中,当一个事物需要修改另一个事物已经修改但未提交的行数据库,就会产生冲突,这时就需要锁机制。提交读事物会等待另一个事物释放行级锁。可能会发生如下情况
如果持有锁的事务回滚,等待的事务会急需修改相关行,就像其他事务从来不存在一样。如果持有锁的事务提交并释放锁,等待事务会在新的改变上继续修改,这可能会导致一个经典的问题,丢失更新(事务提交的更新可能并不在执行查看的更新表中)。例子本文章不再介绍,可以参考英文版的oracle concepts
串行隔离级别中,事务只能看到事物开始时的一致性视图而不是查询开始时。串行事务开来,感觉就像没有其它用户在同时操作数据。串行事务适合的环境:
1,大型事务数据库中,但事务持续时间短,每次只更新少数几行
2,两个事务同时修改同一行的概率很小
3,长事务大都是读操作
串行隔离下,读一致性从语句级别扩展到事务级别。事务期间读到的行数据,都能够被重新读到,任何事物内的查询都能够得到同样的结果。不管查询的时间多长,其它事务的修改在事务内都是不可见的。串行事务不会带来脏读,不可重复读和幻影读。只有在事务开始阶段已提交的行,Oracle才允许串行事务修改这些行。当串行事务试图修改和删除事务开始后其它不同事务提交的数据修改时,会报错ORA-08177:本事务不能串行读取
当一个串行事务报错ORA-08177,程序可做如下几种操作:
1,提交工作到某个时间点
2,执行另外不同的语句,回滚到事务提交之前的某个savepoint
3,回滚事务
只读隔离级别和串行隔离类似,当时只读隔离级别不允许在事务内修改数据,除非当前用户是SYS。因此,只读事务不会产生ORA-08177错误。只读隔离级别在需要产生事务开始时间点的一致性内容的报告时非常有用
oracle通过重建undo段中数据来实现读一致性,由于undo是循环使用方式,因此数据库可能会覆盖undo数据。长时间运行的查询需要的undo数据可能被其他的事务覆盖,产生快照过旧的错误(snapshot too old)。设置一个合理的undo保留的时间,可能有效的避免类似问题
当多个事务处理共享数据时,锁机制可以有效避免破坏性的操作。锁机制在实现并发和一致性方面具有关键作用
根据操作的不同,数据库维护了很多不同类型的锁。一般来说,数据库中有两种类型的锁:排他锁和共享锁。在一个表或者行上等资源上,只能有一个排他锁,但一个资源上却可以有多个共享锁。
oracle处理读和写操作的规则如下
只有写才会锁定行数据
但修改一行数据时,事务只获得这一行相关的锁。通过只锁定一行,数据库能减少争用。在正常情况下,行级锁不会升级到块和表级别
写操作会阻塞修改同一行的写操作
一个事务修改了某行,当另一个事务修改同一行时会被阻塞
读不会阻塞写
由于读操作不会产生行级锁,写操作可以修改这些行。SELECT FORUPDATE操作是个例外,它会锁定读到的所有行
写操作不会阻塞读
当写操作修改了某一行数据时,数据库使用undo数据提供给读操作这一行的一致性视图
注意:
在特殊情况:分布式事务下读操作可能会被写操作阻塞
多用户模式下,数据库通过锁提供防止对共享数据的并发修改。锁实现了数据库的重要特性
一致性:用户正在修改的数据,其它用户不能修改
完整性:数据和结构必须按正确的顺序来反映所有改变
oracle通过锁机制提供并发性、一致性和完整性。锁是数据库自动实现的,不需要用户干预。
假设已有应用程序解决了丢失更新(lostupdate)问题。表9-4描述当两个会话几乎同时修改同一行的情况
当执行sql语句时,oracle自动获取相关的锁资源,例如当修改数据时,会话首先要获取排他锁,锁定相关数据,防止其它事务修改相关数据。由于锁和事务控制的紧密联系,应用程序的设计者只需要正确的定义事务,oracle自动管理相关锁。虽然oracle提供了手工的锁定机制,一般情况下,用户不需要显式的锁定相关资源
oracle自动获取最小级别的限制,限制越少,其它事务的可用性就越高
oracle使用两种类型的锁
排它锁:这种模式防止相关资源被共享。当修改数据时,事务会获取排它锁,此时其它事务不允许修改资源
共享锁:根据操作的不同,允许共享资源。多个用户可以同时读取相关数据,共享锁可以防止写操作获取排它锁。多个事务可以同时获取共享锁
假设一个事务使用select for update选择了一个表中的某些行,事务得到了排它的行级锁和共享行数据的表锁。行级锁运行用户修改其它行,表锁防止其它用户修改表的结构,数据库尽可能允许更多的语句执行
oracle在必要时会有锁转化,数据库自动实现从低级别到高级别的转化。例如用户使用select。。for update查询employee表,接着修改了某些锁定的行数据,在这个例子中,数据库自动从共享锁转化为排他锁。一个事务持有所有修改的行的排他锁,由于行锁是此时最大级别的锁,此时不会锁转化
锁转化和锁升级是不同的概念,锁升级是当一定级别的锁(例如行级锁)达到一定数量时,数据库升级到更高级别的锁(例如表锁)。如果用户锁住了表中的很多行,其它的很多数据库会自动升级为表锁,锁的数量减少了,当锁的级别和限制增加了。
oracle从来不锁升级,锁升级增大了死锁的可能性。例如系统准备为事务1进行锁升级,但是相关资源被事务2持有,而此时如果事务2也需要锁升级持有事务1的相关资源,此时就会产生死锁。
当事务不在需要时,oracle自动释放锁。在大部分场景下,oracle在事务的周期内持有锁,这些锁防止破坏性的冲突,例如并发事务中的脏读,丢失更新和破坏性的DDL
备注:未加索引的外键持有的表锁在语句的执行期间持有,而不是整个事务;使用DBMS_LOCK包用户定义的锁的持有和释放可以跨越锁的边界
事务提交和回滚后,oracle释放事务期间持有的所有锁。回滚到某个保存点(savepoint)的事务,也会释放savepoint后的相关锁定。然而,只有不等待savepont之前锁定的相关资源的事务能够获得相关的资源,等待的事务继续等待直到所有的资源都提交或者回滚。
死锁是两个或多个用户等待互相持有的锁。oracle自动检测死锁,并回滚死锁中的一个语句,释放行锁相关的一系列冲突。
死锁一般发生在默认锁机制被显示覆盖时,由于oracle没有锁升级,且读操作不加锁,使用行级锁而不是页级锁等,死锁发生的几率很小
oracle自动代表事务对资源进行锁定,防止其它事务对同一个资源做排它存取。数据库自动根据操作的类型,对资源加上各种类型的锁定。备注:读操作不会锁定行
oracle锁分为如下分类
DML锁:保护数据,例如表锁锁定这个表,行级锁锁定选择的行
DDL锁:保护对象的定义,例如表和视图的数据字典
系统锁:保护内部数据库的结构,例如数据文件,latch,mutexes和内部锁定,这些都是自动实现的
一个DML锁,又叫做数据锁,用来保证多个用户并发执行时的数据完整性。例如:一个DML锁可以防止两个用户在在线商店购买最后一本书。DML语句自动获取如下类型的锁:行级锁TX和表锁TM
行锁又叫做TX锁,事务锁,是对表中某一行的锁定。当增删改查和select for update等操作修改行数据时,就会产生一个行级锁。事务提交和回滚后,行级锁释放。行级锁采用排队机制防止两个事务同时修改同一行。数据库已排它形式锁住某一行,防止其它事务修改数据,直到事务提交或者回滚。
备注:事务由于实例失败而终止,在整个事务恢复之前,块级别的恢复使行数据可用
如果一个事务得到一个行级锁,事务还会得到一个表锁,表锁防止DDL操作覆盖目前事务的修改的数据。表格9-2显示了修改了表得第三行数据,oracle自动生成一个行级别的排他锁和表上的排它锁
Figure 9-2 Row and Table Locks
表9-6描述了oracle在并发环境下如何使用行级锁。三个会话同时查询同样的行。会话1和2更新不同的行,但不提交,同时,会话3不更新。每个会话看到自己没提交的更新,看不到别的会话未提交的更新
行级锁的存储
其他数据库使用锁管理器在内存中维护所有锁的列表,oracle在锁定行所在的数据块中存储锁的相关信息。数据库使用队列机制获取行级锁,如果事务需要一个未锁定行的锁,那么事务在数据块中在放一个锁,事务修改的每一行都会指向数据块头部(ITL)中的事务ID。当事务结束时,事务ID仍然留在数据块头部的ITL中。如果不同的事务想要修改一行数据,数据库会使用ITL中原来事务的ID,通过查询相关动态视图判断事务是否还存在,及锁是否存在,如果锁仍然是活动的,那么会话排队等待事务结束后的通知,如果锁不活动了,那么,事务得到锁,并更新ITL表
表锁TM
表锁,又叫TM锁,当执行UPDATE,DELETE, MERGE, SELECT with the FOR UPDATE或者lock table,事务会加TM锁。DML操作会使用表锁防止DDL操作和修改表得结构。表锁会以如下的模式持有
行共享(Row Share RS)
又叫做子共享表锁(subshare table lock SS),事务锁住了表中的某些行,并打算更新它们。这种锁是表锁中限制最小的锁,能够实现表级别的最大并发
行排它表锁(Row Exclusive Table LockRX)
又叫做子排它表锁SX,一般表明事务已经更新了表中的某些行数据或者使用了SELECT...FORUPDATE,SX锁运行其它事务增删改查和锁定行,因此SX锁允许多个事务获取同一表中的SX和RS锁
共享表锁
共享表锁允许其它事务查询该表(不使用select...for update),但只有在单个事务持有共享表锁时,才允许更新。由于多个事务同时持有共享表锁,只持有一个共享表锁,是不足以修改表的
共享行排它表锁(SRX)
又叫做共享子排它表锁(SSX),比共享表锁限制更严格一些。一个表中,在一个时间点,只有一个事务能持有SRX锁,SRX锁允许其它事务查询表(除了SELECT...FOR UPDATE),但是不允许更新该表
排它表锁(X)
限制最严格,防止其它事务在表中做DML操作和加任何类型的锁
oracle最大程度上实现了主外键的并发性。外键是否有索引决定了锁定行为。如果外键没有索引,那么子表会被频繁锁定,会导致死锁,并发性会降低。由于这个原因,外键要尽可能添加索引,除非主外键很少被更新和删除
下面两个条件下,数据库会在子表上加全表锁
字表的外键列没有索引
会话修改了父表的主键(例如,删除或者修改了主键的属性)或者在父表合并行。在父表中插入数据不需要字表的表锁
假设表hr.departments是hr.employees的父表,departments表中有未索引的外键department_id,图9-3描述了一个会话修改部门60的主键属性
Figure 9-3 Locking Mechanisms withUnindexed Foreign Key
图9-3,在修改部门60的主键时,数据库在employees中获的一个全表锁,锁允许其它会话查询,当不允许更新employee表。例如,employee表中的手机号不能被更新。表锁在主键修改之后被立即释放。如果部门表中的多个主键需要修改,那么每次修改都需要一个employee表上的锁
备注:子表上的DML操作不需要父表上的表锁
当同时满足下面两个条件时,不会获取子表上的全表锁
1,子表的外键有索引
2,会话修改了父表中的主键(例如,删除一行或者修改主键的值)或者父表中合并行
父表上的锁阻止事务获取排它表锁,但是不阻止在主键修改期间的父表和子表上的DML操作。,如果父表修改主键,子表上更新数据也不会阻止
图9-4描述了子表employee,在department_id上有索引.一个事物删除了departments表中的部门280,这个删除不需要对employees表做全表锁定
如果子表有ON DELETE CASCADE属性设置,删除父表的数据会级联删除子表的数据。例如,删除部门280会导致删除employee表中280部门的所有员工。在这种场景下,等待和锁定规则和先删除主表,再删除父表的规则是一样的
当DDL操作或者关联操作某对象时,DDL锁保护对象的定义。只有在DDL语句中修改或者引用的对象才被锁定,数据库不会锁定整个数据字典。oracle数据库代表DDL事务自动实现DDL锁。用户不能显示获得DDL锁。例如,一个用户创建一个存储过程,数据库自动获得存储过程中引入的对象的DDL锁。DDL锁阻止存储过程编译过程中这些对象的修改和删除
排它DDL锁阻止其他会话获得DDL和DML锁。大部分DDL操作,除了共享DDL锁,需要排它DDL锁,防止其它DDL操作修改相关的对象定义。例如删除一个表的操作会阻止同时在表中添加一列的DDL操作,反之亦然。排它的DDL锁在整个DDL操作时有效,执行结束会自动提交。在获取DDL锁时,如果对象被其它DDL锁持有,那么相关操作会等待,直到DDL锁释放。
共享DDL锁防止其它冲突的DDL操作,但是允许类似的DDL操作并发执行。例如当创建DDL操作时,会对引用的所有表加DDL共享锁,其它事务可以建存储过程时加共享DDL锁,但是不允许加排它DDL锁
sql或者pl/sql会持有应用对象的解析锁。解析锁被用来实现当引用的对象被修改或者删除时,共享sql区域会失效。解析锁之所以易碎,是因为它不允许DDL操作,当DDL冲突时,会被打碎。
oracle使用系统锁保护内部的数据库和内存结构,用户不能操作这些内部锁。
闩是简单的,低级别的序列化机制,用来实现多个用户获取共享数据结构,对象和文件。闩保护共享内存资源,特别是在以下情况下
多个会话同时修改
一个会话读,一个会话修改
读取的时候,内容过期
典型情况下,一个闩保护SGA中的多个对象。例如DBWn和LGWR等后台进程从共享池中内存来创建数据结构。为了分配内存,进程使用共享闩,防止两个进程同时修改共享内存。内存分配之后,其它进程可能需要访问共享池,用来解析的library cache。这种情况下,进程latch只在labrary cache中,而不是整个共享池。
不同于行级锁的排队机制,latch不需要排队机制。当latch可用时,第一个请求到的进程已排它方式使用latch,当进程循环请求latch时,latch回环(spinning)发生。在重新请求latch前,进程会释放cpu,进入latch休眠状态
一般说来,在查找数据结构时,进程使用在极其短的时间内使用latch。例如,当处理一个表更新操作时,数据库会获得并释放数以千记的latch。latch的实现是操作系统相关的,特别是在进程等待latch的具体时间方面
latch的增加意味着并发的降低。例如,过度的硬解析会导致library cachelatch的竞争。v$latch视图有每个latch的详细信息,包括每个latch的请求次数和等待内容
互相排斥对象(mutex)是一种低级别的锁机制,能够防止并发进程访问内存中中的对象过期或者破坏。mutex和latch类似,但是latch一般用来保护一组对象,mutex是保护一个对象
mutex能带来如下好处
降低争用
由于latch保护一组对象,当进程访问任何一个对象都可能有争用现象。由于只保护一个对象,降低了争用的可能性
mutex消耗更少的内容
在共享模式下,mutex运行一定的并发
内部锁是高级别,比latch和mutex更复杂,用于其它用途。数据库中有如下类型的内部锁
数据字典缓存锁
这种锁时间短,当数据字典实体被修改时,用来保护相关内容。这种锁确保语句解析期间,能够看到对象的一致性视图。数据字典锁是共享和排它的。解析结束时,共享锁被释放,DDL操作结束时,排它锁被释放。
文件和日志管理锁
这种锁保护各种文件,例如,内部锁保护控制文件,确保一个时间点只有一个进程能够修改。另外的锁能协调归档和在线日志。当多实例共享模式挂载数据库或者单实例排它挂载时会对数据文件加锁。由于文件锁标识着文件的状态,这些锁持续的时间一般都比较长。
表空间和undo段锁
用来保护表空间和undo段,例如所有实例必须对表空间是否在线达成一致
用户可以手工覆盖oracle定义的默认锁机制,一般在如下情况下发生
应用程序需要事务级别的读一致性和重复读
在这种情况下,查询必须事务期间看到一致性的数据,而不影响其它事务。你可以实现事务级别的读一致性,通过显示的锁定,只读事务和序列化事务,覆盖默认的事务类型
应用程序需要事务排它获取某个资源,所有事务必须等待其它事务结束
你可以在事务和会话级别覆盖数据库的自动锁定,在会话级别,可以使用altersession命令设置,在事务级别,需要使用如下命令
SET TRANSACTION ISOLATIONLEVEL
LOCK TABLE 语句
SELECT FOR UPDATE
通过数据库锁定管理服务,你可以为应用程序定义自己的锁。例如,你可以生成一个锁保护文件系统的消息日志文件。由于用户保留的锁和数据库锁是相同的,它拥有数据库的所有锁定功能,包括死锁检测等。用户锁不能和数据库锁冲突,已前缀UL定义
数据库锁管理服务通过DBMS_LOCK包实现,你可以实现如下功能
请求一个特殊类型的锁;定义一个其它实例和存储过程能识别的锁的名字;修改锁的类型;释放锁。
原文档:
http://docs.oracle.com/cd/E11882_01/server.112/e40540/consist.htm#i13945