11.数据高可用


缓存不命中:把全量数据都放在Redis 集群中,处理读请求的时候,只需要读取Redis,而不用访问数据库。很多大型互联网公司都在使用这种方法
更新缓存:

  • 启动一个更新缓存的服务接收数据变更的消息队列中的消息,然后注意解决消息的可靠性问题即可,这种方式实现起来很简单,也没有什么侵入性。
  • 使用 Binlog 实时更新Redis 缓存(Canal)
    • 由于在整个缓存更新链路上,减少了一个收发消息队列的环节,从MySQL 更新到Redis 更新的时延变得更短,出现故障的可能性也更低,这也是为什么很多大型互联网企业更青睐于采用这种方案的原因。

Canal 详解

它通过模拟MySOL主从复制的交互协议,把自己伪装成一个MySOL的从节点,向 MySOL 主节点发送dump 请求。MySOL 收到请求后,就会向 Canal 开始推送Binlog,Canal 解析Binlog 字节流之后,将其转换为便于读取的结构化数据,供下游程序订阅使用。
Canal有个服务端,在模拟MySOL从节点获得数据库服务器的数据后,我们可以使用一个包含Canal Client的服务程序获得Canal服务端解析出的数据,也可以通过配置让Canal服务端直接将数据发送给MQ,当然我们的Canal Client 程序经过数据处理后也可以发送给MQ。不管是经过Canal Client程序还是直接发给MQ,接下来还可由第三方的服务或者存储系统进行后续处理。

配置

  1. 先开启 Binlog 写入功能,配置binlog-format为ROW模式
  2. 给Canal 设置一个用来复制数据的MySQL账号
  3. 修改canal.properties 文件,比较关键的是canal.destinations
  4. 修改instance.properties
    1. MySQL 主服务的连接配置
    2. 要对哪些相关的业务表进行监视

跨系统实时数据同步

如果是按照关键字搜索,放在ES 中会比放在MySQL 中更合适。所以在大规模系统中,对于海量数据的处理原则都是根据业务对数据查询的需求反过来确定选择什么数据库、如何组织数据结构、如何分片数据等之类的问题,这样才能获得最优的查询性能。在大型互联网企业中、其核心业务数据,以不同的数据结构和存储方式,保存几十甚至上百份,都是非常正常的。

当然为了能够支撑下游的众多数据库,从 Canal 出来的Binlog 数据肯定不能直接写入下游的众多数据库中。原因也很明显:一是写不过来;二是下游的每个数据库,在写入之前可能还要处理一些数据转换和过滤的工作。所以一般我们会增加一个消息队列来解耦上下游

更换数据库

既不能长时间停止服务,也不能丢失数据

不停机更换数据库

我们在设计迁移方案的时候,一定要保证每一步都是可逆的。也就是必须保证,每执行完一个步骤,一旦出现任何问题,都能快速回滚到上一个步骤。

  1. 首先要做的一点是,把旧库的数据全部复制到新库中。因为旧库还在服务线上业务,所以不断会有数据写入旧库,我们不仅要向新库复制数据,还要保证新旧两个库的数据是实时同步的。所以,需要用一个同步程序来实现新旧两个数据库的实时同步。可以使用Binlog 实现两个异构数据库之间数据的实时同步。这一步不需要回滚,因为这里只增加了一个新库和一个同步程序,对系统的旧库和程序没有任何改变。即使新上线的同步程序影响到了旧库,停掉同步程序也就可以了。
  2. 改造DAO层
    1. 支持双写新旧两个库,并且预留热切换开关,能通过开关控制三种写状态:只写旧库、只写新库和同步双写
    2. 支持读取新旧两个库,同样预留热切换开关,控制读取旧库还是新库
  3. 然后上线新版服务,这个时候服务仍然是只读写旧库,不读写新库。让新版服务稳定运行至少一到两周的时间,其间我们不仅要验证新版服务的稳定性,还要验证新旧两个库中的数据是否保持一致。这个过程中,如果新版服务出现任何问题,都要立即下线新版服务,回滚到旧版本的服务
    • 稳定一段时间之后,就可以开启订单服务的双写开关了开启双写开关的同时,需要停掉同步程序。这里有一个需要特别注意的问题是,这里双写的业务逻辑,一定是先写旧库,再写新库,并且以旧库的结果为准
    • 如果旧库写成功,新库写失败,则返回成功,但这个时候要记录日志,后续我们会根据这个日志来验证新库是否还有问题。如果旧库写失败,则直接返回失败,同时也不再写新库了。不能让新库影响到现有业务的可用性和数据准确性。如果出现任何问题都要关闭双写,回滚到只读写旧库的状态。
    • 切换到双写之后,新库与旧库的数据可能会出现不一致的问题。
      • 停止同步程序和开启双写,这两个过程很难做到无缝衔接
      • 双写的第略也不能保证新旧库的强一致性。对于这个问题,我们需要上线一个比对和补偿的程序,用于比对旧库最近的数据变更,然后检查新库中的数据是否一致,如果不一致,则需要进行补偿
    • 开启双写之后,还需要稳定运行至少几周的时间,并且在这期间我们需要不断地检查,以确保不能有旧库写成功、新库写失败的问题。如果在几周之后比对程序发现新旧两个库的数据没有不一致的情况,那就可以认为新旧两个库的数据一直都是保持同步的。
  4. 接下来就可以用类似灰度发布的方式把读请求逐步切换到新库上。同样,运行期间如果出现任何问题,都要再切回到旧库
  5. 全部读请求都切换到新库上之后,其实读写请求已经全部切换到新库上了,虽然实际的切换已经完成,但后续还有需要收尾的步骤。
    • **再稳定一段时间之后,就可以停掉比对程序,把订单服务的写状态改为只写新库。至此,旧库就可以下线了。**注意,在整个迁移过程中,只有这个步骤是不可逆的由于这一步的主要操作就是摘掉已经不再使用的旧库,因此对于正在使用的新库并不会有什么影响,实际出问题的可能性已经非常小了。
    • 双写切换为新库单写这一步不可逆的主要原因是,一旦切换为新库单写,旧库的数据与新库的就不一致了,这种情况是无法再切换回旧库的。所以问题的关键是,切换为新库单写后,需要保证旧库的数据能与新库保持同步。这时双写需要增加一种过渡状态:**从双写以旧库为准过渡到双写以新库为准。然后把比对和补偿程序反过来,用新库的数据补偿旧库的数据。**这样就可以做到一旦出现问题,就直接切回到旧库上。但是这样做一般成本比较高。
  6. 至此们完成了在线更换数据库的全部流程。双写版本的服务也完成了它的历史使命,可以在下一次升级订单服务版本的时候下线双写功能

数据表的变更,如果只是新增表,这个很简单,一般直接回退到旧版本程序即可;但如果牵涉到表字段的变化就麻烦些,但是也可以采用类似的思路,双写新旧表并设计热切换开关

实现比对和补偿程序

这个比对和补偿程序的实现难点在于,我们要比对的是两个随时都在变化的数据厍中的数据。

像订单这类时效性比较强的数据,是比较容易进行比对和补偿的。因为订单一旦完成之后,几乎就不会再改变了,比对和补偿程序就可以根据订单完成时间,每次只比对这个时间窗口内完成的订单。补偿的逻辑也很简单,发现不一致的情况后,直接用旧库的订单数据覆盖新库的订单数据就可以了
这样,切换双写期间,对于少量不一致的订单数据,等到订单完成之后,补偿程序会将其修正。后续在双写的时候只要新库不是频繁写入失败,就可以保证两个库的数据完全一致。
比较麻烦的是更一般的情况,比如像商品信息之类的数据,随时都有可能会发生变化。如果数据上带有更新时间,那么比对程序就可以利用这个更新时间,每次从旧库中读取一个更新时间窗口内的数据,到新库中查找具有相同主键的数据进行比对,如果发现数据不一致,则还要比对一下更新时间。

  • 如果新库数据的更新时间晚于旧库数据,那么很可能是比对期间数据发生了变化,这种情况暂时不要补偿,放到下个时间窗口继续进行比对即可。
  • 时间窗口的结束时间不要选取当前时间,而是要比当前时间早一点,比如1分钟之前,这样就可以避免比对正在写入的数据了。

如果数据没带时间戳信息,那就只能从旧库中读取Binlog,获取数据变化信息后到新库中查找对应的数据进行比对和补偿。

安全地实现数据备份和恢复

一般来说,由存储系统导致的比较严重的损失主要有两种情况。

  1. 数据丢失造成的直接财产损失。比如订单数据丢失造成了大量的坏账。为了避免这种损失,系统需要保证数据的高可靠性
  2. 存储系统的损坏,造成整个业务系统停止服务而带来的损失。比如,电商系统停服期间造成的收入损失。为了避免这种损失,系统需要保证存储服务的高可用性

保证数据安全,最简单且有效的方法就是定期备份数据,这样无论因为出现何种问题而导致的数据损失,都可以通过备份来恢复数据。

最简单的备份方式就是全量备份。备份的时候把所有的数据复制一份,存放到文件中,恢复的时候再把文件中的数据复制回去,这样就可以保证恢复之后,数据库中的数据与备份时的数据是完全一样的。在 MySQL 中,我们可以使用mysqldump 命令执行全量备份。

不过全量备份的代价非常高:首先备份文件包含了数据库中的所有数据,占用的磁盘空间非常大;其次,每次备份操作都要拷贝大量的数据,备份过程中会占用数据库服务器大量的CPU和磁盘IO 资源、同时为了保证数据一致性,备份过程中很有可能会锁表。这此都会导致在备份期间数据库本身的性能严重下降。所以我们不能频繁地对数据库执行全量备份操作。

一般来说,在生产系统中每天执行一次全量备份就已经是非常频繁的了。这就意味着,如果数据库中的数据丢失了就只能恢复到最近一次全量备份的那个时间点,这个时间点之后的数据是无法找回的。也就是说,因为全量备份的代价比较高不能频繁地执行备份操作,所以全量备份不能做到完全无损的恢复

相比于全量备份,增量备份每次只用备份相对于上一次备份发生了变化的那部分数据,所以增量备份的速度更快。

MySQL 自带的 Binlog 就是一种实时的增量备份工具。Binlog 所记录的就是MySQL 数据变更的操作日志。开启 Binlog 之后,MySQL 中数据的每次更新操作,都会记录到Binlog 中。Binlog 是可以回放的,回放Binlog,就相当于是把之前对数据库中所有数据的更新操作,都按顺序重新执行一遍,回放完成之后数据自然就恢复了。这就是Binlog 增量备份的基本原理。很多数据库都有类似于MySQL Binlog 的日志工具,原理也与Binlog 相同,备份和恢复的方法也与之类似。通过定期的全量备份配合 Binlog,我们可以把数据恢复到任意一个时间点

在执行备份和恢复的时候,需要特别注意如下两个要点:

  1. 不要把所有的鸡蛋放在同一个篮子中”,无论是全量备份还是Binlog,都不要与数据库存放在同一个服务器上。最好能存放到不同的机房,甚至不同城市离得越远越好。这样即使出现机房着火、光缆被挖断甚至地震也不怕数据丢失。
  2. 在回放 Binlog 的时候,指定的起始时间可以比全量备份的时间稍微提前一点儿,这样可以确保全量备份之后的所有操作都在恢复的Binlog 范围内,从而保证数据恢复的完整性。
    • 为了确保回放的幂等性,需要将Binlog的格式设置为ROW格式

总结梳理

Canal:把自己伪装成一个MySOL的从节点

  1. 先开启 Binlog 写入功能,配置binlog-format为ROW模式

  2. 给Canal 设置一个用来复制数据的MySQL账号

  3. 修改canal.properties 文件,比较关键的是canal.destinations

  4. 修改instance.properties

    1. MySQL 主服务的连接配置

    2. 要对哪些相关的业务表进行监视

跨系统实时数据同步:息队列来解耦上下游

不停机更换数据库(保证每一步都是可逆的

  1. 把旧库的数据全部复制到新库中,保证新旧两个库的数据是实时同步的

    • 这一步不需要回滚,停掉同步程序就可以了
  2. 改造DAO:支持双写和读取新旧两个库,并预留热切换开关

  3. 只读写旧库,验证新旧两个库中的数据是否保持一致

  4. 开启双写开关,停掉同步程序

    • 先写旧库,再写新库,并且以旧库的结果为准

    • 如果旧库写成功,新库写失败,则返回成功

    • 如果旧库写失败,则直接返回失败,同时也不再写新库了

    • 不能让新库影响到现有业务的可用性和数据准确性

    • 数据不一致:比对和补偿的程序

  5. 灰度发布的方式把读请求逐步切换到新库上

  6. 全部读请求都切换到新库上之后

    • 停掉比对程序,把服务的写状态改为只写新库。至此,旧库就可以下线了(只有这个步骤是不可逆的)

    • 过渡状态:从双写以旧库为准过渡到双写以新库为准。然后把比对和补偿程序反过来,用新库的数据补偿旧库的数据(成本太高)

  7. 在下一次升级服务版本时下线双写功能

关于数据表的变更:

  • 如果只是新增表,直接回退到旧版本程序

  • 如果牵涉到表字段的变化双写新旧表并设计热切换开关

比对和补偿程序

  1. 发现不一致的情况后,直接用旧库的订单数据覆盖新库的订单数据就可以了

  2. 每次从旧库中读取一个更新时间窗口内的数据,到新库中查找具有相同主键的数据进行比对

    1. 如果新库数据的更新时间晚于旧库数据,那么很可能是比对期间数据发生了变化,这种情况暂时不要补偿,放到下个时间窗口继续进行比对即可。

    2. 时间窗口的结束时间不要选取当前时间,而是要比当前时间早一点,比如1分钟之前,这样就可以避免比对正在写入的数据了。

如果数据没带时间戳信息,那就只能从旧库中读取Binlog

数据备份和恢复:通过定期的全量备份配合 Binlog,我们可以把数据恢复到任意一个时间点

  1. 不要把所有的鸡蛋放在同一个篮子中

  2. 指定的起始时间可以比全量备份的时间稍微提前一点儿

    • 为了确保回放的幂等性,需要将Binlog的格式设置为ROW格式

文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录