同步复制和异步复制
同步(synchronously)复制和异步(asynchronously)复制和关键区别在于:请求何时返回给客户端。
- 如果等待某副本写完成后,则该副本为同步复制。
- 如果不等待某副本写完成,则该副本为异步复制。
两者的对比如下:
- 同步复制牺牲了响应延迟和部分可用性(在某些副本有问题时不能完成写入操作),换取了所有副本的一致性(但并不能严格保证)。
- 异步复制放松了一致性,而换来了较低的写入延迟和较高的可用性。
在实践中,会根据对一致性和可用性的要求,进行取舍。针对所有从副本来说,可以有以下选择:
- 全同步:所有的从副本都同步写入。如果副本数过多,可能性能较差,当然也可以做并行化、流水线化处理。
- 半同步:(semi-synchronous),有一些副本为同步,另一些副本为异步。
- 全异步:所有的从副本都异步写入。网络环境比较好的话,可以这么配置。
日志复制
在数据库中,基于领导者的多副本是如何实现的?在不同层次有多种方法,包括:
- 语句层面的复制。
- 预写日志的复制。
- 逻辑日志的复制。
- 触发器的复制。
语句复制
主副本记录下所有更新语句:INSERT、UPDATE 或 DELETE 然后发给从库。主副本在这里类似于充当其他从副本的伪客户端。
但这种方法有一些问题:
- 非确定性函数(nondeterministic) 的语句可能会在不同副本造成不同改动。如 NOW()、RAND()
- 使用自增列,或依赖于现有数据。则不同用户的语句需要完全按相同顺序执行,当有并发事务时,可能会造成不同的执行顺序,进而导致副本不一致。
- 有副作用(触发器、存储过程、UDF)的语句,可能不同副本由于上下文不同,产生的副作用不一样。除非副作用是确定的输出。
预写日志
主流的存储引擎都有预写日志(WAL,为了宕机恢复):
- 对于日志流派(LSM-Tree,如 LevelDB),每次修改先写入 log 文件,防止写入 MemTable 中的数据丢失。
- 对于原地更新流派(B+ Tree),每次修改先写入 WAL,以进行崩溃恢复。
这种结构,天然适合备份同步。本质是因为磁盘的读写特点和网络类似:磁盘是顺序写比较高效,网络是只支持流式写。具体来说,主副本在写入 WAL 时,会同时通过网络发送对应的日志给所有从副本。
逻辑日志复制(基于行)
为了和具体的存储引擎物理格式解耦,在做数据同步时,可以使用不同的日志格式:逻辑日志。
对于关系型数据库来说,行是一个合适的粒度:
- 对于插入行:日志需包含所有列值。
- 对于删除行:日志需要包含待删除行标识,可以是主键,也可以是其他任何可以唯一标识行的信息。
- 对于更新行:日志需要包含待更新行的标志,以及所有列值(至少是要更新的列值)
对于多行修改来说,比如事务,可以在修改之后增加一条事务提交的记录。MySQL 的 binlog 就是这么干的。
使用逻辑日志的好处有:
- 方便新旧版本的代码兼容,更好的进行滚动升级。
- 允许不同副本使用不同的存储引擎。
- 允许导出变动做各种变换。如导出到数据仓库进行离线分析、建立索引、增加缓存等等。
触发器的复制
有些数据库如 Oracle 会提供一些工具。但对于另外一些数据库,可以使用触发器和存储过程。即,将用户代码 hook 到数据库中去执行。
基于触发器的复制,性能较差且更易出错;但是给了用户更多的灵活性。