Design Data Intensive Applications 笔记 (事务)

事务特性:

  • Atomicity (原子性) :
    定义: 用户发起屡次写请求: 中间某次写失败则全部写操做都被回滚数据库

    实现方式: 灾害恢复日志网络

  • Consistency (一致性):
    定义: 应用层面的不变性在事务中被保持并发

  • Isolation (隔离性):
    定义: 多个事务操做同一笔数据的时候效果互不影响, 不产生racing condition
    好比多个事务同时从一个帐号扣减金额,最终金额应该等于初始金额-总扣减金额

隔离级别: 解决事务并发对数据产生影响的问题
常见的并发问题:分布式

  • 脏读 (dirty read):事务2读到事务1未提交的写操做
  • 脏写 (dirty write): 事务2覆盖事务1未提交的写操做. 问题: 多对象更新出现不一致
  • 不可重复读 (non-repeatable read): 事务中和事务结束时读到的数据不一样 (多数为统计查询而且短暂不一致). 可是有时这种不一致性没法容忍. 好比: 数据库复制备份. 大范围分析查询
  • 幻影读 (phantom read): 事务一的写操做的结果影响事务二的查询结果
  • 写误差 (write skew): 两个事务同时检测某个前提条件知足,执行写操做 (未必是同一对象) 并提交, 结果致使前提条件再也不被知足. 好比:医院规定最多不能超过两个医生同时请假, A和B同时申请假期. 前提条件都知足,但结果是两个医生同时请假了.
隔离级别 描述 脏读 不可重复读 幻影读 实现方式
serializable 并发的事务等同于事务一个个执行,彻底没有并发问题,可是性能最差 false false false 实际序列化执行, 二段锁, 可序列化快照分离
repeatable read 快照分离(snapshot isolation): 事务中读取事务开始前的提交数据,即便其余事务在期间改动数据并提交 false false true MVCC (Multi-version concurrency control) 记录数据的多个版本,找到事务开始前的数据版本
read committed 事务提交的数据才能被读到, 写操做只能覆盖提交的数据 false true true 防止脏写: 对事务中写对象持有行级锁直到事务结束. 防止脏读: 记录数据的2个版本,没法解决计数器更新丢失问题
  • Durability (持久性):
    定义: 事务提交以后数据被保存到持久化存储,不会由于宕机而丢失ide

    实现方式:
    单机: 先写日志(write ahead log)
    集群: 保证写操做被持久化到多个节点性能

单对象写:

处理复杂对象如JSON等, 须要保证原子性和隔离性
原子性经过先写日志 (write ahead log) 保证
隔离性经过锁或者compare and swap, 有的数据库提供原子性的read-modify-write操做如increment
不一样于事务, 事务通常是针对多个不一样对象的处理
许多分布式数据库由于实现上的困难只保证单对象写而放弃多对象事务
多对象事务仍然在如下场景须要:
关系型数据库:确保外键一致性被正确保持
文档型数据库: 因为数据被DENORMALIZE,一般一次更新会涉及多个文档,须要保证多个文档被同时正确更新
存在索引的状况下, 保证数据更新和索引的一致性优化

事务错误恢复:

放弃执行到一半的事务并重试
重试可能存在的问题:
事务成功执行但由于网络错误没法获得事务结果:
若是错误是由于系统超载,重试只会让问题变得更糟
重试通常能解决暂时问题, 死锁,网络异常,隔离级别错误等, 若是是永久性错误引发的问题(违反外键约束等), 重试没有意义
重试不能解决事务中数据库以外额外影响, 须要辅助二段式事务等方式
重试中若是客户进程崩溃会致使数据丢失线程

MVCC实现方式:

写数据时,修改的数据被TAG惟一自增交易ID(用于断定交易发生前后), 例如transactionID = 13的事务修改ID为1的数据从V1->V2, 再修改ID为2的数据从V1->V2, 为每次操做生成修改前和修改后两条交易版本数据:
id=1, dataValue=V1, createdBy=5, deleteBy=13
id=1, dataValue=V2, createBy=13, deleteBy=nil
id=2, dataValue=V1, createdBy=5, deleteBy=13
id=2, dataValue=V2, createdBy=13, deleteBy=nil
MVCC数据可见规则:日志

  • 事务开始前,全部正在进行的事务对数据的修改不可见,即便最后事务被提交
  • 取消的事务对数据的修改会被忽略
  • 事务开始后,新的事务(transactionID >= currentTransactionID)对数据修改不可见, 即便最后事务被提交
  • 其余写操做对读事务可见

换而言之, 若是一个对象对于读事务可见: 对象

  • 读数据的事务开始时,建立对象的事务已经提交.
  • 对象没有被删除
  • 对象已经被删除,可是删除对象的事务在读事务开始时还没提交

MVCC索引实现和优化:

  • 索引叶节点须要指向数据全部版本,再经过transactionId过滤
  • 若是更新的数据和原来在同一数据页,则能够避免更新索引 (POSTGRES)
  • 复制有数据更新的索引页和直到根节点的全部索引页,查询时无需作数据过滤,可是须要按期作索引压缩

防止写丢失:

问题定义: 两个事务同时读取数据,进行更新,再将数据写回,其中一个事务的写结果被另外一个覆盖. 好比计数器更新
解决方法:

  • 原子化更新: update table set value = value + 1 where id = 1
  • 显式加锁: select ... for update
  • 自动写丢失检测, 取消并重试冲突事务 (PostgreSQL’s repeatable read, Oracle’s serializable, and SQL Server’s snapshot isolation) (MySQL repeatable read 不支持)
  • 比较写入 (CAS): update table set value = V2 where id = 1 and value = V1 (若是数据库支持从旧快照读则无效)

在集群复制场景下(多主复制或无主复制): 比较写入方法不能正常工做, LAST WRITE WIN策略会形成写丢失. 原子化更新有效

防止写误差:

冲突实体化 (materialize conflict): 将写冲突转化为一系列的主键锁. 好比建立预订房间记录(房间号+时间范围),并对预订记录加锁. 将并发控制处理渗漏到业务逻辑中,不推荐.

实际序列化执行 (actual serialization):
实现方式:

  • 单线程按顺序处理事务. (REDIS, VOLTDB 等)
  • 存储过程执行事务
  • 数据分片: 对每一个分片顺序执行事务 (跨分片事务性能有较大损失)
    适合场景:
  • 事务影响的数据集不大,可存储在内存中
  • 单个事务读写很少
  • 存储过程没法实现用户交互式事务
  • 对事务处理吞吐量要求不高

二阶段锁 (two phase locking):
实现方式:

  • 事务读数据时必须得到读锁(共享模式),多个读锁可共存,可是若是有写锁,读操做必须等待
  • 事务写数据时必须得到写锁(排他模式),若是有其余锁(读或写), 写操做必须等待
  • 事务先读后写, 读锁必须升级为写锁
  • 锁必须被持有直到事务结束

缺点:

  • 并发度降低致使吞吐量降低
  • 执行延迟不可预测
  • 死锁更加容易发生

前提锁 (predicate lock):
锁符合查询条件的对象,读写锁获取和二阶段锁相似

索引范围锁 (index range lock):
比predicate lock范围大,不精确匹配全部查询条件而匹配其中某个索引查询,以下午1:00-2:00对某个房间的预订. 直接对房间号加锁或者对时间范围加锁.
若是没有找到合适的索引,对全表加共享锁.

可序列化快照分离 (serializable snapshot isolation):

  • 兼顾性能和序列性
  • 采起乐观并发控制,在事务提交的时候检查冲突,而后取消事务并重试
  • 乐观并发控制适合数据争用不太多的场景,不然会致使大量事务取消重试

实现方式:
根据事务开始前预设定的前提决定事务是否能提交, (好比每一个房间/时间段预订数<=1). 而后检查查询结果是否被改变.
事务1读到一个旧的MVCC版本(数据已经被另外一个事务修改但未提交), 再提交时发现新的MVCC版本已经被事务2提交, 而且后续事务1也对这个数据进行了修改. 则事务1必须回滚
事务1开始后事务2修改了数据. 并早于事务2提交. 若是后续事务1也对这个数据进行了修改. 则事务1必须回滚

总结

我的阅读事务章节的一点感想:事务控制无非两种: 乐观: 经过冲突检测决定事务是否回滚. 悲观: 经过加锁阻止并发发生.从另外一个维度看, 不一样的隔离级别决定了两个事务对于数据库的读写操做是否阻塞,隔离级别越低,阻塞的场景越少任何隔离级别一个事务的读操做不会阻塞读读阻塞写, 写阻塞读 (Serializable级别 -- two phase locking)写阻塞读, 但读不阻塞写写阻塞写 ()

相关文章
相关标签/搜索