DDIA读书笔记 第五章 数据同步

数据同步
多主
无主
主从关系
同步方式
同步同步
异步同步
半同步同步
同步滞后
read-after-write 一致性
单调读一致性
前缀一致读
实现
基于语句
基于WAL
基于行
拓扑结构
环形
星形
全链接
并发写冲突
避免冲突
覆盖
合并
失效节点上线

1 一主多从

通常主节点负责写入,从节点负责读取,主节点的信息需要同步给从节点。

主从同步按照同步方式可以分为同步同步,异步同步,半同步同步。同步同步需要等待从节点返回确认信息,才可以进行下一次同步,异步同步则不需要。半同步同步则是部分节点同步同步,部分节点异步同步,在吞吐量和一致性方面做了一个折中。

1.1 同步的实现

基于语句的同步

基于语句的同步,就是将主节点的操作语句发送给从节点,让从节点执行。这种方式在有些场景下不适用:

  • 不确定性语句,例如 NOW()
  • 自增列,或者依赖现有数据(UPDATE … WHERE … )
    MySQL 采用基于语句的同步,如果有不确定性操作,则自动切换到基于行的同步

基于 WAL 的同步

WAL 预写日志,记录了某块磁盘的某个字节的变化,主从节点基于 WAL 进行同步,会使得同步和存储引擎紧密耦合,不方便版本升级。

基于行的同步

同步行请求,包括行插入、行删除、行更新,相较于基于预写日志的同步,解耦了存储引擎,
逻辑日志与存储引擎解耦

1.2 同步滞后

因为主节点的信息同步到从节点有时延,所以客户端从从节点上读取的数据不一定是最新的。这就是同步滞后问题。

read-after-write

一个用户写完后再读,主节点未能在读之前将数据同步给从节点,给用户造成了写入没成功的错觉,如下图。
DDIA读书笔记 第五章 数据同步_第1张图片
为了保证 read-after-write 一致性,可以采取如下方案:

  • 如果用户访问可能会修改的数据,从主节点读取,否则从从节点读取。例如用户配置页面,用户访问自己的用户配置信息时从主节点读取,访问其他人的则从从节点。
  • 写入时附带时间戳,如果一分钟内有读取操作,则从主节点读取。

单调读

主节点写入后,同步到两个从节点的时延不一致,导致第一次读有效,第二次读无效,如下图。
DDIA读书笔记 第五章 数据同步_第2张图片
为了保证单调读一致性,可以让用户固定从某一个从节点读,例如对用户 ID 做 hash 路由到从节点。

前缀一致读

前缀一致读指的是,读取的顺序和写入的顺序相同,这在后面章节会讲到。

有关同步滞后问题,可以从应用层出发去解决,因为应用层可以判断数据是否是最新的。事务也可以一定程度地保证一致性。

2 多主

单个数据中心使用多主节点没什么意义,多主节点一般适用于多个数据中心的场景。

一种极端的场景就是一个客户端设备就是一个数据中心,比如笔记软件,在手机或笔记本离线时也要有修改能力,上线后再进行主节点之间的同步。

2.1 写冲突

多主节点很容易面临写冲突问题。

应用层避免冲突

应用层对特定的写操作总是通过某一个主节点,这就需要路由层面的逻辑来实现。例如对于每个用户只能修改自己数据的场景,把同一用户路由到同一主节点,就可以避免写冲突。

解决冲突

  • 比较写请求 ID 大小
    给写请求分配一个唯一的 ID,可以是时间戳,哈希值,随机数,ID 更大的写请求覆盖冲突的写请求。会造成数据丢失。
    如果 ID 是时间戳,则是 Last Write Win(LWW),最后写入者获胜,顾名思义,最后写入的会覆盖,例如缓存系统的场景这种方法是可行的。
  • 比较节点 ID 大小
    某些节点永远优先级更高。同样会造成数据丢失。
  • merge
    合并冲突的写请求,这在无主章节会进一步说明。
  • 自定义解决冲突
    在检测到冲突后,可以返回给应用层调用相应逻辑解决冲突,甚至提示用户冲突在哪里,让用户自己解决。例如 github 在执行 git rebase 时产生的冲突会给出提示,由用户自己编辑文件解决冲突再提交。

2.2 拓扑结构

主节点之间可以用环形、星形、全链接的拓扑结构。
DDIA读书笔记 第五章 数据同步_第3张图片
环形和星形的结构,写请求需要多次转发才能到达所有主节点,且某一个节点故障,会影响其它节点的日志同步。全链接拓扑则没有这个问题,但是由于一个节点会收到多个节点的操作请求,需要确保这些请求是正确有序的,这一点在无主章节进一步探究。

3 无主

共 n 个节点,写入时需写入 w 个节点,读取则从 r 个节点读取。n w r 需满足:n < w + r 。这样写入的节点集合和读取的节点集合必然有交集,所以一定能读取到最新值。这称之为 quorum 机制。

那么如何确认哪个节点的值更新呢?版本号或者时间戳都是供选择的方案。

3.1 失效节点重新上线

  • 读时修复
    客户端在读取时,可以检测到持有旧值的节点,此时写入新值。该方法适合频繁读取的场景。如果有些数据一直没被读取,就可能存有非常旧的数值。

  • 反熵
    后台进程不断检测版本差异并进行同步。

3.2 并发写

并发写即多个客户端对同一个键同时写入,但因为时钟同步和网络阻塞的缘故,“同时”很难界定,因此如果事件A和B互相意识不到对方,则A和B就是并发的,而不要求时间上的同时发生。

处理并发写的难点之一是确定操作的依赖关系,或者说,先后顺序。下图用一个购物车的例子说明了一种基于版本号的单节点并发处理算法。

DDIA读书笔记 第五章 数据同步_第4张图片
算法流程如下:

  • 每个键都有一个版本号,写入时版本号递增,并保存版本号和写入的值。
  • 客户端读取时,服务端返回所有(未被覆盖的)版本号和对应的值。且写之前必须读。
  • 写请求包括读到的版本号,以及读到的值和新值合并后的值。写请求的响应和读请求响应一样,返回所有版本号和值。
  • 客户端收到写请求响应后,会对多个版本做合并操作。下一次的写请求发送的就是更新的版本号和合并后的值了。
  • 服务端在接收一个写请求后,如果这个写请求的版本号是旧的,就会覆盖旧版本和旧值。因为客户端在收到写请求响应后会进行合并,旧的版本号和旧值也就没必要保存了。

对于删除的项,不是简单地从数据库里删除,因为这样在 merge 时又会把删除的项加进来,正确的操作是标记删除,在合并时再删除。

以上所述均是针对单个主节点的情况,如果多个主节点,一个版本号就没法解决了。一个解决方案是版本矢量,每个主节点的每个键都有一个版本号。

你可能感兴趣的:(Engine,数据库)