0%

Designing Data-Intensive Applications 7 - 事务

深入理解事务

ACID的含义

原子性(Atomicity)

在出错时中止事务, 并将部分完成的写入全部丢弃

可中止性。简化了排错过程,事务如果中止, 应用可以安全地重试

一致性(Consistency)

对数据有特定的预期状态, 任何数据更改必须满足这些状态约束(或者恒等条件)

本质上要求应用层来维护一致

应用程序可能借助数据库提供的原子性和隔离性, 以达到一致性, 但一致性本身并不源于数据库

数据库只能完成针对某些特定类型的恒等约束条件,例如外健约束或唯一性约束

持久性(Durability)

一旦事务提交成功, 即使存在硬件故障或数据库崩溃,事务所写入的任何数据不会消失

没有哪一项技术可以提供绝对的持久性保证, 应该组合使用

数据写入硬盘, 使用预写日志, 复制

隔离性(Isolation)

并发执行的多个事务相互隔离, 不能互相交叉

由于串行化隔离的性能原因,常用的是更弱级别的隔离

单对象与多对象事务操作

单对象写入

原子性

基于日志恢复(Btree)

原子自增

原子比较-设置 (compare-and-set)

多对象事务的必要性

通常意义上的事务针对的是多个对象, 将多个操作聚合为一个逻辑执行单元

关系数据库的外健

文档数据模型,可能更新多个文档

图数据库的顶点和边

带有二级索引的数据库, 更新值时同步更新索引

处理错误与中止

中止后不撤销:某些无主节点复制的数据存储,遇到错误, 不会撤销已完成的操作

中止后撤销但不重试:Rails ActiveRecord, Django在事务异常时简单地抛出堆栈信息, 不会重试

理念:支持安全的重试机制才是中止流程的重点

重试的局限性

执行成功,但返回客户端时网络意外, 重试可能导致重复执行, 需要额外应用级重复数据删除

设置重试次数:系统超负荷时, 一直重试会加重负担

重试仅对临时性故障有意义,出现永久故障时无意义

两阶段提交,避免副作用。比如每次事务中国呢调用其它系统,发送邮件等

客户端重试时失败, 待写入数据可能丢失

事务的意义

事务是一个抽象层,提供安全性保证,简化应用层的编程模型

NoSQL的兴起,很多新一代数据库完全放弃事务支持,或者替换为更弱的保证

事务有优势,也有局限性,需要权衡

弱隔离级别

读提交

防止脏读

读数据库时,只能看到已成功提交的数据。不会看到部分更新或会被回滚的数据

实现:对于待更新对象, 维护旧值和当前持锁事务要设置的新值两个版本。事务提交前读旧值, 提交后切换到新值

防止脏写

写数据库时,只会覆盖已成功提交的数据。避免不同事务的写入混杂

实现: 行级锁。数据库自动完成

不可重复读/读倾斜(时间异常)

备份场景

分析查询和完整性检查场景

有些场景不能容忍这种暂时的不一致状态

快照级别隔离与重复读

实现快照级别隔离

MVCC

一致性快照的可见性原则

1. 进行中的其它事务,不可见

2. 所有中止事务所做的修改,不可见

3. 较晚事务ID所做任何修改, 无论是否提交,不可见

4. 除上述之外的所有写入都对应用查询可见

4.1 事务开始时, 创建该对象的事务已经完成提交

4.2 对象没有标记为删除,或者标记了,但删除事务在当前事务开始前未完成提交

快照级别隔离如何支持索引

方案1(PostgreSQL将同对象的不同版本放在一个内存页面上): 索引指向对象的所有版本, 过滤当前事务不可见的那些版本。后台垃圾回收进程删除旧对象版本时,同时删除对应索引

方案2(CouchDB,Datomic,LMDB): B-tree,追加/写时复制。更新时,每个写入事务创建新的B-tree root,代表该时刻数据库的一致性快照

可重复读与命名混淆

命名混淆:快照隔离技术, Oracle称为可串行化, PostgreSQL和MySQL称为可重复读

原因: SQL标准只定义了可重复读, 未定义快照级别隔离

防止更新丢失(写事务并发)

原子写操作(数据库)

许多数据库提供了原子更新操作 eg: update counters set value = value + 1 where key = 'foo';

实现1: 对读取对象加独占锁,更新提交前不会有其它事务可读它(游标稳定性)

实现2: 强制所有的原子操作都在单线程上执行

显式加锁(应用层)

应用程序显式锁定待更新的对象,然后执行"读-修改-写回"

自动检测更新丢失(借助快照级别隔离来高效执行)

原子操作和锁都是强制"读-修改-写回"操作序列串行执行

换个思路: 先让他们并发执行, 如果事务管理器检测到了更新丢失风险, 中止当前事务, 强制回退到安全的"读-修改-写回"

PostgreSQL的可重复读, Oracle的可串行化,SQL Server的快照级别隔离都支持

MySQL InnoDB的可重复读不支持

原子比较和设置(无事务支持)

先执行CAS,如果不成功则回退到"读-修改-写回"

注意: 如果where语句运行在旧快照上,可能无法防止另一个并发写入。所以要仔细检查CAS操作的安全运行条件

冲突解决与复制(多副本)

保留多个冲突版本,由应用层逻辑或依靠特定的数据结构来解决

如果操作顺序无关,则原子操作可行

LWW,容易丢失更新,但是目前许多多副本数据库的默认配置

写倾斜与幻读

定义写倾斜/幻读

一个事务的写入改变了另一个事务查询结果

更新丢失问题。如果两个事务读取相同的一组对象,然后更新其中一部分

写倾斜:不同的事务可能更新不同的对象

脏写、更新丢失: 不同的事务更新的是同一个对象

之前可选方案的可行性

原子操作,不可行,因为涉及到多个对象

定义数据库约束条件,然后数据库代为检查、执行约束, 不可行, 大多数场景的类型约束数据库不支持

自动检测更新丢失,不可行,目前都不支持自动检测写倾斜(需要真正的可串行化隔离)

对事务依赖的行来显式加锁,可行, FOR UPDATE

更多写倾斜的案例

为何产生写倾斜

实体化冲突

把幻读问题 转变为 针对数据库中一组具体行的锁冲突问题

并发控制机制 降级为 数据模型

串行化

实际串行执行

采用存储过程封装事务

避免等待IO(用户交互,应用程序与数据库间多次网络通信)

存储过程的优缺点

数据库厂商的存储过程语言大多语义丑陋,过时(最新的存储过程使用现有通用语言: VoltDB使用Java或Groovy,Datomic使用Java或Clojure,Redis使用Lua)

数据库中运行代码难以管理(调试,版本控制,部署,测试,监控)

设计不好,所有应用程序都受影响

分区

高写入需求下,单线程事务处理容易成为瓶颈

分区,扩展到多个CPU核和多节点

跨分区事务由额外的协调开销,所以适合大量键值数据,不适合带有多个二级索引的数据

串行执行小结

为什么现在能实现,为什么以前都要靠多线程并发来提升性能

内存越来越便宜, 事务所需数据都可加载到内存中

OLTP事务通常执行很快

Redis, VoltDB/H-store, Datomic等采用串行, 避免锁开销, 吞吐量上限为单个CPU核的吞吐量

两阶段加锁(two-phase locking, 2PL)

实现2PL

多个事务可以同时读取同一对象,但只要出现任何写操作,则必须加锁以独占访问(读写也会互斥)

1. 每个对象都有一个读写锁来隔离读写操作

2. 事务要读取对象,必须先获得读锁,可以有多个事务同时获得一个对象的读锁

3. 如果某个事务已经获得对象的写锁, 其它事务必须等待

5. 如果事务先读取,然后尝试写入,必须先把读锁升级为写锁

4. 事务要修改对象, 必须先获取写锁,如果已被加写锁,必须等待

6. 事务获得锁后, 一直持有到事务结束。第一阶段即事务执行之前要获取锁,第二阶段即事务结束时要释放锁。

容易死锁,数据库自动检测死锁, 强行中止其中一个

2PL的性能

锁的获取和释放本身开销

数据库访问延迟具有非常大的不确定性

降低了事务的并发性

死锁更为频繁,如果由于死锁而被强行中止,应用层必须从头重试

谓词锁

可串行化隔离也要防止幻读问题

作用于某些搜索条件的所有查询对象

谓词锁可以保护那些上不存在但可能马上会被插入的对象

两阶段加锁+谓词锁,可以防止所有竞争条件,隔离变得真正可串行化

索引区间锁

对谓词锁的简化,因为谓词锁性能不佳

将保护的对象扩大化,不像谓词锁那么精确,但是开销低

如果没有合适的索引,可以回退到对整个表施加共享锁

可串行化的快照隔离(SSI)

悲观与乐观的并发控制

2PL是悲观并发控制机制

SSI是乐观并发控制: 如果可能发生潜在冲突,事务继续执行而不中止;事务提交时检查是否冲突,是的话再中止并重试。

缺点: 如果冲突很多,则性能不佳; 如果系统已接近最大吞吐量,反复重试会使情况更糟

优点: 如果系统有足够的性能提升空间,且事务竞争不大, 乐观并发控制会高效很多

基于过期的条件做决定

数据库必须检测事务是否会修改其他事务的查询结果

1. 检测是否读取了(即将)过期的MVCC对象,即读取前已经有未提交的写入

2.检测写是否影响了之前的读,即读取后,又有新的写入

可串行化快照隔离的性能

优点:事务不需要等待其它事务所持有的锁,只读查询不需要任何锁

可以突破单个CPU核的限制,FoundationDB将冲突检测分区,事务分区

事务中止的比例会显著影响SSI的性能表现

边界条件小结

脏读

脏写

读倾斜(不可重复读)

更新丢失

写倾斜

幻读