本文最初发布于王斌的个人博客,经原作者授权由 InfoQ 中文站翻译并分享,原文地址:
https://www.binwang.me/2020-11-29-Keep-Data-Consistency-During-Database-Migration.html
当一个系统存在很长一段时间后,经常会使用更新的技术来提高性能、可维护性或添加新特性。其中一个变化可能会是使用哪个数据库。这可能是最困难的改变。在迁移过程中,有两个数据源,这使得该系统成为一个分布式系统。在分布式系统中,保持数据一致非常困难,而且很容易出错。在本文中,我们将探索一种在迁移期间保持数据一致性的方法,并且这种方法的停机时间较短。
前提条件
为了使用本文描述的方法,需要满足一些要求:
源数据库支持捕获数据更改(CDC)的方法,如 MySQL 的 bin log。
源数据库可以导出一致的数据,并且可以在数据更改日志中标记位置。
目标数据库支持 ACID 事务。
源和目标数据库都支持读写权限控制。
步骤
制定以下迁移步骤的两个基本想法:
在给定的时间点上,客户端只向其中一个数据库写入,从而避免了分布式事务易出错、处理速度慢的问题。
通过设置数据库权限来实现数据库切换。这比从客户端代码切换要快,而且更容易确保所有客户端都切换。
下面是具体步骤。
1.将源数据库转储到目标数据库
首先,我们需要源数据库可以导出一致的数据。标记好已转储的位置。例如,在 MySQL 中,可以在使用mysqldump
转储数据库时带上--master-data
选项,这样生成的文件中就会记录 bin log 日志的位置(使用文档)。从源数据库获得所有数据后,可以将它们插入目标数据库。
因为这是第一步,所以即使失败了也很容易处理:重新开始即可。因此,重要的是,在导入转储数据时,任何错误都要捕获。
2. 捕获源数据库的更改
下一步是捕获源数据库的更改。例如,在 MySQL 中,可以使用bin log捕获更改并将其插入到目标数据库中。因为上一步记录了开始位置,所以我们知道从哪里开始解析和导入更改。
在导入时保持更改顺序非常重要。所以最好是只使用一个进程来解析和导入更改。这个步骤非常具有挑战性:这一步性能很重要。同步所有更改的时间就是迁移所需的停机时间。
我们还需要确保,即使出现系统故障,也不会错过任何更改或多次导入任何更改。因此,记录更改日志的位置非常重要。我们可以使用与导入数据相同的事务将位置写入目标数据库,这非常方便。这样,位置就可以与我们导入的数据保持同步。
3. 拒绝客户端到目标数据库的写操作
使用单一数据源是一种保持数据一致性的简单方法。到目前为止,我们使用源数据库作为单一数据源,并将更改同步到目标数据库。我们不想让其他写操作把目标数据库搞乱。因此,我们需要设置目标数据库的权限来拒绝来自客户端的所有写操作。例如,在 MySQL 中,只向客户端授予表的 select 权限,并拒绝其他操作。我们允许读取权限,这样在下一步中就可以比较读取结果。
4. 修改客户端,同时对两个数据库进行读取和写入
下一步是让客户端同时对源和目标数据库进行读取和写入。
我们希望首先读/写源数据库。如果没有权限错误,则使用此结果,否则使用来自目标数据库的读/写结果。
对目标数据库的读/写操作有两个目的:
在切换到目标数据库之前,我们可以通过比较读结果和写操作来验证目标数据库是否按预期工作。注意,目标数据库可能存在同步延迟,因此结果可能会不同。但是我们可以根据相同结果的百分比来理解其正确性。
在我们切换到目标数据库之后,读/写结果将被视为真实结果。
如果你想确保目标数据库能够处理这些负载,那么在一段时间内允许对目标数据库进行读/写操作是个好主意,但这只是作为一个验证,在那之后,目标数据库中的数据会不一致。因此,在我们验证了目标数据库能够处理系统流量之后,我们需要清理目标数据库,并再次从步骤 1 开始。(在这些步骤中,我们不需要修改客户端代码)。
对于错误处理,有两个关键点:
如果源数据库有权限错误,则只使用目标数据库的结果。抛出源数据库的其他错误。
如果没有使用这个结果,则忽略目标数据库的错误,但要确保记录这些错误,以便它不会影响当前操作,同时还要确保切换之前没有错误。
客户端代码如下:
5.拒绝客户端对源数据库的访问,并等待更改同步
在对目标数据库的读写操作有信心之后,就可以进行切换了。我们通过更改数据库权限来切换数据库。首先,我们拒绝客户端对源数据库的所有访问。然后等待更改完全同步到目标数据库。在此期间,系统是停机的。因此,从源数据库到目标数据库的更改同步速度决定了它的停机时间。
6.允许写入目标数据库
在目标数据库完全同步之后,我们可以为所有客户端赋予目标数据库权限。在此之后,系统就再次在线了,数据库已完全切换。
7.可选:如果出现任何错误,则回退到源数据库
如果到目前为止一切顺利,那就太好了。但情况并非总是如此。也许目标数据库无法处理新的流量(这就是我们说步骤 4 的测试很重要的原因),在这种情况下,我们就需要回退到源数据库。
如果是在迁移期间丢失了提交的数据,那不是什么大问题,回退也比较简单:
允许访问源数据库。在此之后,客户端应该再次使用源数据库。
清理目标数据库,并重新从头开始。
如果保存提交的数据并保持一致性非常重要,那么在第 5 步之前,我们应该设置一种机制来捕获从目标数据库到源数据库的更改,并在第 6 步之后标记更改位置。那么,回退步骤将是下面这样:
拒绝对目标数据库的所有写操作。
从目标数据库向源数据库同步(在同步完成之后停止同步程序)。
允许访问源数据库。
清理目标数据库并重新开始。
从目标数据库到源数据库的同步非常危险且难以测试,因此,在步骤 4 中测试目标数据库能否处理新的流量就非常重要。
8. 清理客户端代码
切换到目标数据库之后,我们就可以清除访问源数据库的代码了。这样,数据库迁移就大功告成了!
评论