首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

DDIA 读书笔记(6)数据库事务

事务是简化数据内部很多复杂问题的首选机制,一个事务要么成功要么失败,应用层不需要担心一部分成功的尴尬情况。本文通过讨论的事务的使用场景以及解决问题的方式来理解事务。

ACID 的不精确解释

ACID 的定义最早在 1983 年提出,提出至今一直只是个概念,各家数据库的执行力度不一,导致现在的 ACID 更像是个营销术语。

ACID 四个特性中,着重需要讨论的点在于原子性和隔离性。一致性应该是由应用层来维护,而持久性是数据库最基本的要求。

原子性描述的是事务的完整性,即要么成功,要么失败的特性。它要求事务在出错时需要终止,并将已完成的部分写入回滚。

隔离性是解决并发问题的核心特性,它要求多个事务并发执行时,最终结果要跟串行执行的结果完全相同。实践中一般不会真正地串行化执行,而是用一些更弱的形式保证。

弱隔离级别

串行化隔离是最强的隔离级别,但是会严重影响性能,因此主流数据库都会实现较弱的隔离级别。

读-提交

读-提交是最基本的隔离级别,它只提供两个保证

  • 读取时只能看到已经成功提交的数据(脏读)
  • 写入时只会覆盖已经成功提交的数据(脏写)

防止脏写使用的是行级锁,同时只有一个事务能够持有锁。但是防止脏写不能用锁,会带来严重的性能问题,因此主流方案是同时维护旧值和当前持有锁的事务要设的新值两个版本。

快照级别隔离与可重复读

读提交保证了最基本的事务隔离,但仍然有些问题无法解决。

Alice 两个账户各有500美元,现在有一笔事务需要转账 100 美元,如果她在转账期间查看两个账户的余额,会出现一种可能,一个账户的读请求发生在事务完成之后,余额变成了 400 美元,一个账户的读请求发生在事务完成之前,看到的还是 500 美元。

这个问题称为不可重复读,对于大多数业务场景来说这个异常是可以接受的,用户刷新之后就能看到正确的数据。但是有些场景是不能容忍的,比如备份数据库或是数据分析和完整性检查,这类查询需要读取大量数据并进行计算,因为时间点导致的读取不一致会导致毫无意义的结果。

解决这类问题需要更高的隔离级别,称之为 “快照级别隔离”,为了实现这一级别,数据库需要考虑多个事务在不同时间点读取数据,并维护数据的多个版本,因此这个技术也称为多版本并发控制(MVCC)。

通过 MVCC 技术,每个事务都有一个 ID,发起查询的事务开始时间在转账事务之前,因此 ID 为 13 的事务所做的修改对于查询都是不可见的。

并发写冲突问题

写事务并发会带来一系列值得关注的问题,脏写只是其中的一个特例。最著名的就是更新丢失问题

应用程序从数据库中读取一个值,作出修改并写回新值,由于隔离性的原因,会导致并发的两个事务同时读取到旧值并写回,导致其中一个事务的结果丢失。比如递增计数器,更新账户余额等

最好的办法是数据库提供原子操作,并将类似的逻辑下沉到数据库中,如果条件不允许,就需要应用层显示加锁

写倾斜与幻读

还有一个更微妙的写冲突问题,设想这样一个例子,医院的值班系统要求同一时间至少有一名医生在值班,当前时间段有两名医生在列表内,然后两个人同时申请调班,事务同时发起,都查询到有两个人值班,然后更新自己的记录,并成功提交。最终结果是没有医生值班,违背了一开始的要求。这种两笔事务更新不同对象导致的问题称为“写倾斜”。而这种一个事务中的写入改变了另一个事务的查询结果的现象,称为“幻读”。

这类问题没有很好的解决方案,主要靠应用层自己去加锁或者在索引上加锁。

串行化

串行化是最强的隔离级别,它可以防止所有可能的并发问题。串行化执行有三种主流的方案

  • 真-串行执行
  • 两阶段加锁
  • 乐观并发控制(可串行化的快照隔离)

其中真串行执行在内存数据库中有着不错的表现,在线 OLTP 业务靠单线程往往能支撑不错的吞吐量。而对于强调持久化的数据库来说,串行执行会有很多限制。

两阶段加锁

两阶段加锁(2PL)是最主流的串行化算法,与防止脏读的锁相比强制性更高,多个事务可以同时读取同一个对象,但只要出现写操作,就必须加锁独占访问。

即 读操作与读操作不互斥,读操作与写操作互斥,写操作与写操作互斥。因为使用了较为复杂的锁机制,因此在实际运行中很容易出现死锁导致事务终止,应用层需要做好重试机制。

两阶段加锁的性能依旧不太乐观,一个大的事务可能会阻塞后续大量的事务。在并发量的大的情况下,死锁可能会非常频繁,导致性能问题。

可串行化的快照隔离

可串行化的快照隔离(SSI)是一种新出的算法,提供了完整的可串行性保证。相比两阶段加锁的悲观控制,SSI 是基于乐观并发控制的方法实现的,它仍然是通过数据库的一致性快照来执行事务,在快照隔离的基础上增加了相关算法来检测写入之间的串行化冲突来决定终止哪些事务。

要解决写倾斜的问题,数据库需要假定对查询结果的任何变化都会导致写事务失效,即以下两种情况

  • 读取的对象是否是一个已经过期或即将过期的 MVCC 对象
  • 当前的写入是否影响即将完成的读取(读取之后又有新的写入)

对于第一种情况,数据库需要跟踪那些由于 MVCC 可见性而被忽略的操作,当事务提交时,如果有被忽略的写操作已经提交,就需要终止当前事务

对于第二种情况,可以使用索引区间锁来达成目的,当写事务尝试修改对象时,首先检测索引上的其他读事务,并在这些读事务提交时通知冲突,

基于乐观控制的 SSI 不会阻塞事务的执行,因此性能会高很多,但也有写限制。如果并发过高导致事务的终止比例过高,也会显著影响 SSI 的性能表现。因此 SSI 要求读-写型事务尽可能的要尖端,以避免冲突。

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/7f2af6a37f0624f751ed15caa
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券