当前位置: 代码迷 >> 综合 >> 《设计数据密集型应用》- Designing Data-Intensive Application - 第7章 事务 读书笔记
  详细解决方案

《设计数据密集型应用》- Designing Data-Intensive Application - 第7章 事务 读书笔记

热度:2   发布时间:2024-03-07 08:59:45.0

在这里插入图片描述

事务

事务的棘手性

ACID

单对象和多对象操作

  • 单对象写入

    • 对单节点上的单个对象(例如键值对)上提供原子性和隔离性

      • 原子性通过日志做崩溃恢复
      • 隔离性通过加锁实现
  • 多对象事务

    • 关系型数据库更新带有外键的表
    • 文档型数据同时更新多份文档
    • 二级索引与数据的同时更新
  • 处理错误和中止

    • 对于已经提交的事务,如果在给客户端返回结果的网络中断,如果此时重试事务,则需要去重机制
    • 如果是由网络拥塞导致的重试,则需要限制重试次数
    • 非必要问题不用重试,如违反约束不需要重试
    • 两阶段提交

弱隔离级别

相比可序列化的强隔离性之外都算做弱隔离级别

读已提交

  • 脏写

    • 同时写入后面的写入覆盖前一个
    • 解决方法:加行锁
  • 脏读

    • 读到未提交的数据

    • 解决方法

      • 与解决脏写的方法一样,加行锁,但如果写事务数据时间过长,读请求会有较大的时间延迟

      • 保存数据的旧值,在事务未提交时读到旧值,事务提交后读到新值

        • 如果读取事务的时间范围内出现了写事务的提交,则读事务中相同的两次或多次读取事件所读到的数据可能会不同

快照隔离和可重复读

  • 支持数据库:PostgreSQL oracle MySQL(innodb) SQL Server

  • 实现快照隔离

    • 对于PostgreSQL,按如下方法实现MVCC

      • 基于txid事务ID实现,每个事务开始时均有一个唯一的TXID,约40亿次后溢出
      • 表中每一行数据都有created_by与deleted_by字段,分别存储写入时的txid
      • 删除时标记deleted_by,不实际删除,在稍候的时间,当确定没有事务再访问已标记删除的行时,垃圾回收器将该行移除,释放空间
      • 对于UPDATE操作实际执行了两步操作:DELETE INSERT
    • 拓展(非本书内容):MYSQL5.6 MVCC实现

      • 仅存在于读已提交与可重复读两种隔离级别,特别地,对于可重复读,使用了MVCC+行锁实现,读时不加锁,写时加锁

      • 在MYSQL中如果事务更改了某行,则会针对该行生成UNDOLOG(记录了该行之前的记录内容),在发生回滚时,将UNDOLOG该行的记录恢复

        • 行锁 间隙锁

          • MYSQL:基于事务ID,比如undolog中存了v7这条回滚数据,而现在最小的活跃事务ID是v8,那v7这条数据就不可能再被读到,就可以删除了。
  • 观察一致性快照的可见性规则

    • 列出所有尚且未提交或尚未终止的事务列表,即使这些事务后来提交了,忽略这些写入
    • 具有较晚事务ID的都将被忽略
  • 索引和快照隔离

    • PostgreSQL:同一对象的不同版本可以放入同一个页面中,PostgreSQL的优化可以避免更新索引
    • CouchDB,Datomic和LMDB:为修改页面创建一个索引副本,从父级到根进行级联指向更新指向新版本
  • 可重复读与命名混淆

防止丢失更新

  • 定义

    • 并发写入冲突(丢失)问题(并发的数据库读值–更改数据–写回数据操作)
  • 解决方法

    • 原子写

      • 如UPDATE XX SET a=a+1

        • 实现方案

          • 在对象上加排它锁
          • 强制原子操作在单一的线程上执行
    • 显式锁定

      • 在查询范围上加排它锁

    • 自动检测丢失更新

      • PostgreSQL的可重复读,Oracle的可串行化和SQL Server的快照隔离级别,都会自动检测到丢失更新,并中止惹麻烦的事务。但是,MySQL/InnoDB的可重复读并不会检测丢失更新
    • Compare and Set

    • 冲突解决和复制

      • 允许写入多个冲突值,并在应用层进行合并
    • 写入偏差与幻读

      • 场景

        • 一个SELECT查询找出符合条件的行,并检查是否符合一些要求。
        • 按照第一个查询的结果,应用代码决定是否继续。
        • 如果应用决定继续操作,就执行写入(插入、更新或删除),并提交事务。
        • 如两个人同时抢购1件商品,二人都先查到还要一件库存,在应用程序当中,二者均向数据库发出减库存的写入请求,此时会出现库存异常
      • 快照隔离解决了读事务中出现的幻读情况,但会出现写入偏差

可序列化

背景:

  • 很难检查代码在特定隔离级别下是否安全
  • 并发测试困难

真的串行执行

  • 单线程执行并发事务

  • 产生背景

    • RAM足够便宜,可将事务需要的活跃的数据集放在RAM当中,而不是之前放在磁盘上,执行效率提高了
    • OLTP事务通常执行速度很快,只进行少量的读写操作
  • 实例:VoltDB/H-Store,Redis和Datomic

  • 在存储过程中封装事务

  • 存储过程的优缺点

  • 分区

    • 对于可以把数据划分为多个分区,并且每个事务只在一个分区上进行,即可实现事务随分区数量增加而线性扩展。对于跨分区的事务,则不能通过增加机器提升性能

二阶段锁定

  • 相比较快照隔离,二阶段锁定基于读不阻塞写,写阻塞读的方案,解决了写入偏差与丢失更新

  • 实现2PL

    • 分为共享锁与排它锁

      • 多个事务可同时持有共享锁
      • 有一个写入事件则升级共享锁为排它锁,该锁阻塞其它读取与写入,直到事务完成
      • 性能相比较弱隔离级别差得多
  • 谓词锁

    • 为了防止范围查询出现幻读的情况,需要一种范围锁(非指定对象,比如表中某一行的锁),需要实现以下两点

      • 排它阻塞读–任一读取事务必须等到持有排它锁的事务结束后才可进行读取操作
      • 任意一种锁(排它与共享)阻塞排它锁
  • 索引范围锁(间隙锁)

    • 谓词锁由于性能不佳(锁粒度过小,检查锁匹配非常耗时),多数数据库实现了索引范围锁

      • 如房间预订请求,查询条件为某时间段与房间号,如果表分别在时间段和房间号建立了索引,则索引范围锁会将其中一个索引(时间段或房间号)上附加共享锁,如果其它事务的写入操作需要修改该索引,则它会等到上述共享锁释放

可序列化快照隔离

  • 在快照隔离的基础上,SSI添加了一种算法来检测写入之间的序列化冲突,并确定要中止哪些事务。(乐观锁)

    • 如果出现了大并发写入事务争抢同一写入对象,会导致一大部分写入事务需要终止,如果系统已经接近最大吞吐,则重试事务可能会导致性能变差
  • 快照隔离会出现写入偏差,检测方法有两种

    • 检测对旧MVCC对象版本的读取

      • 在提交事务时检测是否有任何被忽略的写入现在已经被提交,如果有则终止事务并重试;如果其它提交的事务只包含查询,由于其不会产生写入偏差,则不中止事务

    • 检测影响先前读取的写入

      • 借助索引,如果事务中的UPDATE语句的WHERE条件列有索引,则事务提交时记录下该修改值,在其它事务也基于该WHERE条件进行更新时,则通知该事务所读取的数据已经不是最新的

  • 性能

    • 在SSI中,一个事务不需要阻塞等待另一个事务所持有的锁,与两阶段锁定相比,其查询延迟可预测更好;与串行执行相比,其性能不局限于单个CPU核的吞吐量
  相关解决方案