加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

Postgresql的并发(一)

发布时间:2020-12-13 17:08:58 所属栏目:百科 来源:网络整理
导读:转载请注明URL:http://write.blog.csdn.net/postedit/50747829 翻译:卧龙居(pijing) 13.1.简介 PostgreSQL提供了非常丰富的工具给开发者,以供开发者控制对于数据的并发访问。在内部,数据的一致性由多版本模型控制(多版本并发控制--英文全称:Multivers

转载请注明URL:http://write.blog.csdn.net/postedit/50747829

翻译:卧龙居(pijing)


13.1.简介



PostgreSQL提供了非常丰富的工具给开发者,以供开发者控制对于数据的并发访问。在内部,数据的一致性由多版本模型控制(多版本并发控制--英文全称:Multiversion Concurrent Control,MVCC)。这就意味着,每一条SQL语句将一份数据的快照(一个数据库版本)视为是一段时间之前的,而不用管底层数据的当前状态。这样可以阻止更新同一行的数据的并发事务导致的数据的不一致,提供了每一个数据库会话的事务的隔离。MVCC,避开了传统数据库系统的锁机制,最小化锁争夺,从而提供了多用户环境下的合理的性能。
使用MVCC模型进行并发控制,而不使用锁,最大的优势在于MVCC读数据和写数据并不冲突,所以读并不阻碍写,写并不阻碍读。PostgreSQL通过使用可序列化快照隔离,提供了最严格的事务隔离保证。
对于并不需要严格的事务隔离并且期望能够清晰的控制特定冲突的应用来说,它们也是可以使用PostgreSQL中的表锁和行锁的。然而,合理的使用MVCC将会比锁有更好的性能。另外,应用定义决策锁提供了一种不绑定单个事务的请求锁的机制。


13.2.事务隔离
SQL标准定义了4种事务隔离等级。最严格的是可序列化,它的定义是可序列化事务集合的任意并发执行都保证和有序执行一样产生相同的效果。另外三个则是从现象上定义的,即在任意等级上都不应该出现并发事务之间的交互。标准是基于可序列化的定义来的,在所有等级上这些现象都不应该出现(这是毋庸置疑的--如果事务执行的效果要和有序执行的效果保持一致,你怎么可能看到任何交互现象呢?)


在一些等级不允许出现的现象包括:
脏读:一个事务读取了一个并发的事务尚未提交的数据
不可重复读:一个事务再次读取它曾经读取过的数据,发现这些数据已经被另一个并发事务修改过了(在初次读之后提交了)
幻像读:一个事务再次执行查询,得到满足查询条件的行集合,发现这些满足条件的航集合已经被另一个刚刚提交事务改变了


4种事务隔离等级以及相应的表现如表格 13-1所示。


Table 13-1.标准SQL事务隔离等级




在PostgreSQL中,你可以请求4种事务隔离等级中的任意一种。但是在内部,其实只有3种隔离等级,即读已提交(Read Committed)、可重复读(Repeatable Read)、可序列化(Serializable)。当你选择读未提交(Read Uncommitted)其实你真实得到的是读已提交(Read Committed)。在PostgreSQL中对于可重复读(Repeatable Read)的实现中,幻像读是不可能出现的,所以真实的隔离等级其实比你选择的更为严格。这是被SQL标准允许的:4个隔离等级仅仅定义了哪些现象必须不能出现,但他们没有定义哪些现象必须出现。PostgreSQL仅支持3种隔离等级的原因在于,这是将标准隔离等级和MVCC架构相对应的明智途径。下面将详细介绍可用的隔离等级。


如果要设置事务的隔离等级,请使用SET TRANSACTION命令。


13.2.1.读已提交隔离等级(Read Committed Isolation Level)
读已提交隔离等级(Read Committed Isolation Level)是PostgreSQL数据库中默认的隔离等级。当用这个隔离等级时,一个SELECT查询(没有FOR UPDATE/SHARE)只能看到查询开始前已经提交的数据;它不能够看到在当前事务过程中任何其他并发事务没有提交的数据或者变化。实际上,SELECT查询看到的是查询开始时的数据库快照。然而,SELECT可以看到它自己所在的事务中在它之前执行的的update,不管它自己的事务是否有被提交。同时需要注意,如果有一个并发的事务提交在第一个SELECT开始后和在第二个SELECT开始前,那么这两个连续的SELECT查询也许看到不同的数据,就算它们在同一个事务中。
UPDATE,DELETE,SELECT FOR UPDATE和SELECT FOR SHARE命令和SELECT在检索目标行时表现一样;它们仅仅只能找到在查询开始时的目标行。然而,这样一个目标行也许已经被UPDATE了(或者被DELETE了或者被锁了),在这种情况下,接下来的UPDATE将会等待第一个UPDATE事务提交或者回滚。如果第一个UPDATE事务回滚了,那么它的影响是被忽略的,那么第二个UPDATE将可以更新原来就检索到的行。如果第一个UDPATE事务提交了,那么第二个UPDATE将会忽略掉第一个UPDATER删除掉的行,将它的操作在已经更新过的行上进行。命令中的查询条件(where)将会再次评估,看更新过的行是否还满足检索条件。如果满足,第二个UPDATE将会在已经更新过的行上进行操作。如果用了SELECT FOR UDPATE和SELECT FOR SHARE,这就意味着行的更新版本已经被锁定了然后再返回给用户。


因为上面的原则,在一个更新操作中可能会见到不一致的快照:它可以看到它尝试UPDATE的行上其他并发的UPDATE命令(其他事务已提交)造成的影响,但它不能看到这些其他命令对于数据库的其他行的影响。这个行为导致读已提交模式在复杂的检索场景中不很合适;然而,它对于简单的场景还是非常合适的。例如,可以看一下更新银行账户的事务:


如果两个如此的并发事务都试图更新账户12345,我们很明显期望第二个事务在已经更新后的账号行上进行更新。因为每一个命令都只影响预先定义好的行,所以更新并不会导致任何不一致的问题。

在更复杂的使用场景下,使用读已更新模式可能会产生非预期的结果。例如,想象一下一个DELETE命令操作的数据是被另一个命令同时操作和删除的,例如,假定website是一个两行的表格website.hits分别为9和10。


DELETE不会被执行,就算在UPDATE之前或者之后有website.hits=10的行。这种情况的产生在于,在UPDATE之前的行值为9会被忽略,而当UPDATE完成,DELETE获得锁,此时新行的值不再是10而是11,不再符合条件了。
这是因为读已提交模式的每一行命令都处理的是当前所有其他事务已经提交后的数据的新快照,同一个事务的顺序的命令将会看到其他已提交并发事务的影响。上述问题的重点在于是否一个单独的命令看到的是数据库的完全一致的视图。
读已提交模式提供的部分事务隔离对于大多数应用都是合适的,这个模式非常快,而且使用起来也非常简单;然而,它也并不适用于所有场景。进行更复杂的查询和更新的应用,也许需要比读已提交模式提供的更严格一致的数据库视图。


13.2.2.可重复读隔离等级(Repeatable Read Isolation Level)
可重复读隔离等级只能看到事务开始前所有已提交的数据;它不能看到未提交的数据,也不能看到在事务过程中被其他并发事务提交的数据。(然而,查询可以读到它自己所在事务的updated的数据,尽管他们尚未提交)这是比SQL标准所要求的更强的保障,阻止了表格13-1描述的所有现象。和上面提到的一样,这是被SQL标准允许的,SQL标准只定义了每个隔离等级必须提供的最小保障。
这个等级和读已更新不同,它读到的是事务开始前的数据快照,而不是每一条查询开始前的数据的快照。因此,在一个事务中的所有SELECT读到的都会是同一份数据,它们不能看到事务开始之后其他并发事务提交或作出的改变。
使用这一个等级的应用必须准备好处理序列化异常(serialization failures),在发生此异常时需要重新提交事务。

UPDATE,SELECT FOR UDPATE和SELECT FOR SHARE命令与SELECT命令在检索目标行上表现一样。他们只会检索在事务开始前已经提交过的目标行。然而,这个目标行也许已经被UPDATE了(或者被DELETE了或者被锁了)。在这种情况下,可重复读事务将会等待第一个UPDATE事务提交或者回滚(如果它依然在执行状态的话)。如果第一个UPDATE回滚了,那么它的影响就被忽略了,可重复读事务将会在原先检索的行上做更新。但是如果第一个UDPATE提交了(并且更新或者删除了行,而不仅仅是锁住了它),则可重复读事务则会回滚并且抛出异常:


因为可重复读事务不能修改或者锁住在本事务开始后被其他并发事务修改过的行。
当一个应用收到了错误消息,它要么就放弃本事务,要么就重新提交整个事务。第二次时,事务将会将之前已经提交的作为它的初始化数据库视图的一部分,所以用一个新的事务用一个新的行版本并没有逻辑冲突。
注意只有更新事务可能需要重新提交;只读的事务是不会遇到序列化冲突的。
可重复读模式提供了严格的保证,使得每一个事务看到的是稳定的数据库视图(译者注:因为事务中的所有语句看到的数据,都是事务开始前所有已提交的数据;之后不会再变化了)。然后,这种视图的一致性,对于一些同样等级上有序执行的的并发事务而言并不总是需要的。例如,在这个等级上甚至只读的事务可能知道一个控制记录被更新了表明有一个批处理结束了,但是不能够看到这个记录的任何细节,因为它读到的是控制记录的早期版本。尝试使用这个隔离等级加强业务规则,如果不能够小心的使用显式锁处理事务冲突的话,也许不能够运行得很正确。


13.2.3.可序列化隔离等级(Serializable Isolation Level)
可序列化隔离等级提供了最严格的事务隔离。这个等级模拟了所有已提交事务的顺序事务,就好像这些事务是一个接一个的执行一样,序列化的,而非并发的。然而,和可重复读等级一样,使用这一个等级的应用必须准备重新提交事务以应对序列化失败(serialization failures)。事实上,这个隔离等级运行得和可重复读一样,除了它会监控可序列化事务集合的并发操作可能导致不一致的场景。这个监控并不会引起任何超过可重复读的阻碍,但是这儿确实有一个监控,用于监视可能导致序列化异常的场景从而触发序列化失败(serialization failure)。

例如,有个表mytable,初始化包括:


想象有一个可序列化事务A计算:


然后插入result(30)到一个新行中(class=2)。并发的,可序列化事务B计算:


然后得到的结果300,插入一个新行中(class=1),然后两个事务都尝试提交。如果这两个事务运行在可重复读隔离等级下,那么都是运行提交的;但是因为这儿并没有一个顺序保证执行结果的一致性,使用可序列化事务将会允许一个事务提交,而另一个将会回滚并且返回如下所示的消息:

这是因为如果A在B之前执行,B将会计算出结果为330而非300;同样另一种顺序也会导致A计算的总数的不同。 当依赖可序列化事务阻止异常时,最重要的是从表格中读取的任何数据都不被认为是合法的直至事务可以成功的提交。甚至对于只读事务也是如此,除了只读事务中的读可延迟被认为是合法的,因为这样的事务将会持续等待直至它可以没有任何问题的请求到数据快照。在另外一些用例中,应用不应该依赖于可能之后就会被终止的事务的结果;而是应该尝试重复提交事务直至成功。 为了保证可序列化,PostgreSQL使用了谓词锁,这就意味着它可以保持这个锁以决定何时进行写操作从而对并发事务的读结果造成影响。在PostgreSQL中锁并不会导致阻塞,因此不会再任何地方造成死锁。它们只是定义和标识可序列化并发事务之间的依赖,用于确定特定的组合会导致序列化异常。与之相对的是,读已提交或者可重复读,为了确保数据的一致性,可能需要申请一个表锁--用于阻碍其他用户尝试用这个表的行为,或者它也许会用SELECT FOR UPDATE或者SELECT FOR SHARE可能不会阻塞其他事务但是会导致硬盘访问。 PostgreSQL中的谓词锁,和其他数据库系统的一样,基于本事务访问的数据。它们将会在pg_locks系统视图里展示出来,mode字段的值为SIReadLock。一个查询中申请的特定的锁,依赖于这个查询的执行规划,细粒度的锁(如元组锁)也许和粗粒度的锁(例如页锁)联合使用,用于避免跟踪锁内存耗尽。一个只读事务如果它检测到并没有会导致序列化异常的冲突后,也许可以在完成前释放它的SIRead锁。事实上,只读事务通常可以不用任何谓词锁。如果你明确请求SERIALIZABLE READ ONLY DEFERRABL事务,它将会阻塞,直至真正的建立起来(这只是可序列化事务阻塞的例子,在可重复读里没有)。在另一方面,SIRead锁通常被保持直至事务被提交,直至读写事务完成。 使用可序列化事务保证一致性可以简化开发。并发可序列化事务的任意集合都可以保证与顺序的执行它们产生相同的效果,就像上面写的那样,它将会自己做正确的事情,你也可以相信它在任意组合的可序列化事务中都会做正确的事情,甚至在不知道其他事务可能在做什么的情况下。用这种技术时最重要的是捕获序列化失败(返回的SQLSTATE是'40001),因为预测什么事务也许有读写依赖并且需要回滚以阻止序列化异常是非常困难的。检测读写依赖代价非常大,与当捕获到序列化失败重启事务一样。在某些环境下使用显示锁以及SELECT FOR UPDATE或SELECT FOR SHARE来平衡应对消耗和阻塞是比较好的方式。 当依赖可序列化事务来进行并发控制时,需要考虑如下: .声明事务为只读的(READ ONLY)当需要时 .控制处于活动的连接数目,如果需要的话可以使用连接池;性能通常是重要的,但在一个繁忙的系统中使用可序列化事务是特别重要的 .不要在一个事务中放入超过需要的东西 .不要让连接处于闲置状态 .显式锁、SELECT FOR UPDATE、 SELECT FOR SHARE在可序列化事务中不再被需要,因为可序列化事务会自动的提供保护 .如果系统同时使用页级别的谓词锁和单个关系级别的谓词锁,由于谓词锁表内存不足,也许会出现序列化失败数目增多,你可以避免此问题通过增加max_pred_locks_per_transaction. .顺序扫描通常需要关系级别的谓词锁,会导致序列化失败比例上升;通过鼓励使用索引扫描降低random_page_cost/提升cpu_tuple_cost是非常有用的。在查询执行时一定要衡量回滚事务的任何减少以及重启以应对总体的变化。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读