【书籍】设计数据密集型应用(DDIA)ch5-复制

124 阅读4分钟

同步复制和异步复制

同步(synchronously)复制异步(asynchronously)复制和关键区别在于:请求何时返回给客户端。

  1. 如果等待某副本写完成后,则该副本为同步复制。
  2. 如果不等待某副本写完成,则该副本为异步复制。

image.png

两者的对比如下:

  1. 同步复制牺牲了响应延迟部分可用性(在某些副本有问题时不能完成写入操作),换取了所有副本的一致性(但并不能严格保证)。
  2. 异步复制放松了一致性,而换来了较低的写入延迟和较高的可用性。

在实践中,会根据对一致性和可用性的要求,进行取舍。针对所有从副本来说,可以有以下选择:

  1. 全同步:所有的从副本都同步写入。如果副本数过多,可能性能较差,当然也可以做并行化、流水线化处理。
  2. 半同步:(semi-synchronous),有一些副本为同步,另一些副本为异步。
  3. 全异步:所有的从副本都异步写入。网络环境比较好的话,可以这么配置。

日志复制

在数据库中,基于领导者的多副本是如何实现的?在不同层次有多种方法,包括:

  1. 语句层面的复制。
  2. 预写日志的复制
  3. 逻辑日志的复制
  4. 触发器的复制

语句复制

主副本记录下所有更新语句:INSERTUPDATEDELETE 然后发给从库。主副本在这里类似于充当其他从副本的伪客户端

但这种方法有一些问题:

  1. 非确定性函数(nondeterministic) 的语句可能会在不同副本造成不同改动。如 NOW()、RAND()
  2. 使用自增列,或依赖于现有数据。则不同用户的语句需要完全按相同顺序执行,当有并发事务时,可能会造成不同的执行顺序,进而导致副本不一致。
  3. 有副作用(触发器、存储过程、UDF)的语句,可能不同副本由于上下文不同,产生的副作用不一样。除非副作用是确定的输出。

预写日志

主流的存储引擎都有预写日志(WAL,为了宕机恢复):

  1. 对于日志流派(LSM-Tree,如 LevelDB),每次修改先写入 log 文件,防止写入 MemTable 中的数据丢失。
  2. 对于原地更新流派(B+ Tree),每次修改先写入 WAL,以进行崩溃恢复。

这种结构,天然适合备份同步。本质是因为磁盘的读写特点和网络类似:磁盘是顺序写比较高效,网络是只支持流式写。具体来说,主副本在写入 WAL 时,会同时通过网络发送对应的日志给所有从副本。

逻辑日志复制(基于行)

为了和具体的存储引擎物理格式解耦,在做数据同步时,可以使用不同的日志格式:逻辑日志

对于关系型数据库来说,行是一个合适的粒度:

  1. 对于插入行:日志需包含所有列值。
  2. 对于删除行:日志需要包含待删除行标识,可以是主键,也可以是其他任何可以唯一标识行的信息。
  3. 对于更新行:日志需要包含待更新行的标志,以及所有列值(至少是要更新的列值)

对于多行修改来说,比如事务,可以在修改之后增加一条事务提交的记录。MySQL 的 binlog 就是这么干的。

使用逻辑日志的好处有:

  1. 方便新旧版本的代码兼容,更好的进行滚动升级。
  2. 允许不同副本使用不同的存储引擎。
  3. 允许导出变动做各种变换。如导出到数据仓库进行离线分析、建立索引、增加缓存等等。

触发器的复制

有些数据库如 Oracle 会提供一些工具。但对于另外一些数据库,可以使用触发器和存储过程。即,将用户代码 hook 到数据库中去执行。

基于触发器的复制,性能较差且更易出错;但是给了用户更多的灵活性。