《Designing Data Intensive Applications》读书笔记 - 事务

最近读到的最好的一本技术书,评价非常高,堪称经典。书还未读完,收获很大,值得记录的很多。
这一篇博客先讲事务,真心觉得这本书解释的非常清楚,胜过很多的博客。

事务
不是所有的的应用都支持事务,有些应用为了高性能或者高可用性就完全舍弃了事务。是否需要事务,首先需要理解事务能够解决的问题以及需要付出的成本。

ACID

事务的四个特性:原子性、一致性、隔离性、持久性。

  1. 原子性 原子性通常来说事物不可分割,但是无论如何,事务都无法做到像原子一样不可拆分,所以作者认为更合理的名字是 abortability,就是事务在出错时可中止。
  2. 一致性 关于数据的某些陈述一直为真,比如转账事务,转账前后的总金额应该是相同的。作者认为一致性的定义取决于应用对不可变性的定义,应该由应用来定义事务的正确性,而不是由数据库来定义。
  3. 隔离性 并发执行的事务互相隔离,或者说可顺序执行。并发事务的结果应该与顺序执行的结果一致。但是实际上,顺序级的隔离很少见,因为会导致性能问题。
  4. 持久性 事务提交后,数据应该持久化,即使系统崩溃也不会丢失。对于单节点数据库,意味着数据写入磁盘,对于分布式数据库,意味着数据写入多个节点。这儿要强调的是完全的持久性是不存在的。

单个对象写也涉及到原子性和隔离性,但是事务一般理解为对多个对象的一组操作。

事务的关键特性是出错时可以中止并且可以进行重试。但是一些常见的 ORM 框架并不会进行重试,而是直接抛出异常。 This is a shame, because the whole point of aborts is to enable safe retries.
重试看起来很容易,但是有些问题:

  1. 如果是网络问题,重试可能会导致重复操作。
  2. 如果是过载问题,重试只会加重负载。
  3. 只有暂时性的问题才能重试,如果是永久性的问题,重试是没有意义的。
  4. 如果事务中有副作用,比如事务失败发邮件,重试可能会导致重复发送邮件。
  5. If the client process fails while retrying, any data it was trying to write to the database is lost 没 Get 到这句话的意思。

弱隔离级别

序列化隔离级别是最高的,但是实际上很少使用,因为性能问题。大多数数据库使用的是弱隔离级别。

读提交(Read Commited)

最基本的事务隔离级别,确保:

  1. 没有脏读
  2. 没有脏写,注意并不包括两个 Counter 自增的问题

防止脏写最常见的方法是使用行级锁,事务修改一行时必须先获取到锁,其他事务修改同一行时会被阻塞。
防止脏读使用锁就不现实了,因为一个耗时的写事务会导致读事务长时间等待。

Snapshot Isolation or Repeatable Read (可重复读)

读提交包括了所有事务要求的特性,可以中止,阻止读取未提交数据,脏写。但是仍有其它并发问题,比如不可重复读,事务前后读取结果不一样,虽然最终数据一致,但是用户可能困惑,在备份数据时可能出现不一致问题。
书中并没有说明前后两次读是在一个事务中,所举的场景比如:备份,分析查询和数据完整性检查,都是现实中的普通场景。

可重复读的实现一般是使用多版本并发控制(MVCC),简单来说,对于每个事务都对应着一个唯一的增长的事务 ID,每条被写入的数据都有一个事务 ID。
书中使用两个字段:created_bydelete_by,更新对应的是删除和添加。对应的可见性规则:

  1. 每个事务开始时,数据库列出所有未提交的事务以及已经中止的事务,这些事务的写入都被忽略,即便后续提交。
  2. 任何中止事务的写入被忽略
  3. 任何 transaction ID 大于当前事务 ID 的写入被忽略
  4. 所有其它写入都是可见的

MVCC 的索引 一种方法是过滤掉不可见数据,另一种方法是就像 immutable 树,索引树对每个事务都有不同根节点。

Snapshot Isolation 有不同的名字,Oracle 叫做 serializable,PostgreSQL 和 MySQL 是 repeatable read

Lost Updates (更新丢失)

一些场景:

  1. 自增或者根据当前值更新
  2. 集合添加,读取集合之后添加
  3. 两个用户同时编辑 wiki 页面,每个用户保存整个页面内容,覆盖当前数据库内容

解决方案:

  1. 原子写 UPDATE counters SET value = value + 1 WHERE ...
  2. 显式锁 SELECT ... FOR UPDATE 就是很容易遗漏
  3. 自动检测 数据库可以执行非常高效的检测(PostgreSQL, Oracle, SQL Server), MySQL 没有

Write Skew and Phantoms

书中例子,两个 on-call 医生同时请假,系统约束必须有一个医生在岗。在事务中,先查询在岗人数,如果人数大于 2,取消 one on-call。 如果发生竞争条件,有可能两个人都请假,导致系统没有医生在岗。
这个可以说是 更新丢失的一个泛化,先查询再更新,更新之后不再满足约束。还有一个订会议室的例子,先查询给定会议室在时间区间内是否有预定,如果没有,预定会议室。

这儿的主要问题是没有一个合适的对象可以加锁, Materializing conflicts 方案,维护会议室和 time slots (时间段) 的表,这样就可以给查询到的行加锁。这种方案容易出错,通常又不够优雅,不如直接考虑序列化。

序列化

真正的序列化

真正的序列化变得可行因为 RAM 够大, OLTP 很少部分数据进行读写而且读写一般很快。

两段锁 (Two-Phase Locking)

在事务开始时,获取所有需要的锁,直到事务结束才释放锁。死锁更容易发生,毕竟事务开始时就获取所有锁,一直持有到事务结束。

Serializable Snapshot Isolation (SSI)

SSI 是基于 Snapshot Isolation 的,增加算法检测冲突。
检测 Query 结果变化

  1. 检测对过时 MVCC 对象版本的数据读
  2. 检测读后写

工程细节影响算法实际表现。事务读写数据的粒度,如果跟踪很细,识别需要中止的事务就比较精准,反之就比较粗糙,中止事务的可能性就比较大。
对读比较多的应用而言,SSI 非常有吸引力

总结

读完这部分再看一些关于事务的博客总觉得他们写的不够精准。这本书本身讲的也很清楚,很容易理解,非常值得一读。

评论