前言最近在我的技术群里,有小伙伴问大家一个问题:如何保证Mongodb和数据库双写的数据一致性?群里的朋友讨论的这个技术点引起了我的兴趣。其实我在实际工作中也在一些业务场景中使用了Mongodb,也遇到过双写的数据一致性问题。今天跟大家分享一下,这类问题的解决方法,希望对大家有所帮助。一、常见的误区很多朋友看到双写数据一致性问题,首先想到的就是Redis和数据库之间的数据双写一致性问题。有朋友认为Redis与数据库的数据双写一致性问题与Mongodb与数据库的数据双写一致性问题是同一个问题。但是,如果您考虑使用它们的场景,就会发现一些差异。1.1我们如何使用缓存?Redis缓存可以提高我们系统的性能。一般情况下,如果有用户请求,先检查缓存,如果缓存中有数据就直接返回。如果缓存中不存在,则再次检查数据库。如果存在于数据库中,则将数据放入缓存中并返回。如果数据库中不存在,则直接返回失败。流程图如下:有了缓存,可以减轻数据库的压力,提高系统性能。通常,为了保证缓存和数据双写的数据一致性,最常用的技术方案是:延迟双删除。有兴趣的朋友可以看看我的另一篇文章《如何保证数据库和缓存双写一致性?》,里面有很详细的介绍。1.2我们如何使用MongoDB?MongoDB是一种高度可用的分布式文档数据库,用于存储大量数据。文档存储一般以类似json的格式存储,存储的内容为文档类型。通常,我们用它来存储大数据或者json格式的数据。对于用户写数据的请求,核心数据会写入数据库,非核心数据可能会以json格式写入MongoDB。流程图如下:另外,在数据库的表中,保存了MongoDB相关文档的id。用户请求读取数据,首先会读取数据库中的数据,然后通过文档的id读取MongoDB中的数据。流程图如下:这样可以保证核心属性不会丢失,同时存储用户传入的更大数据,两全其美。Redis和MongoDB在我们的实际工作中有不同的用途,这就导致了他们对双写数据一致性问题的解决方案也不同。下面我们一起来看看,如何保证MongoDB的数据一致性和数据库的双写呢?2、如何保证双写一致性?目前MongoDB和数据库数据双写最常用的方案有以下两种。2.1先写数据库,再写MongoDB。这个解决方案是最简单的。先把核心数据写入数据库,再把非核心数据写入MongoDB。流程图如下:如果一些业务场景对数据完整性要求不高,即非核心数据可选,也可以采用该方案。但是,如果某些业务场景对数据完整性要求比较高,这种方案可能会出现问题。数据库刚刚保存核心数据时,网络异常,程序保存MongoDB非核心数据失败。但是MongoDB并没有抛出异常,数据库中保存的数据无法回滚。这样,数据保存在数据库中,而没有保存在MongoDB中,导致MongoDB中的非核心数据丢失。因此,该方案在实际工作中使用不多。2.2先写MongoDB,再写数据库本方案中,先在MongoDB中写非核心数据,再在数据库中写核心数据。流程图如下:关键问题来了:如果在MongoDB中写入非核心数据成功,而在数据库中写入核心数据失败怎么办?此时MongoDB中的非核心数据不会回滚。可能会出现数据保存在MongoDB中而数据库中没有的问题,也会出现数据不一致的情况。答:我们忘记了一个前提。查询MongoDB文档中的数据,必须传递保存在数据库表中的mongoid。但是如果mongoid没有成功保存到数据库中,那么MongoDB文档中的数据就永远查询不到了。也就是说,在这种情况下,MongoDB文档保存了垃圾数据,但对实际业务没有影响。这种方案可以解决双写数据的一致性问题,但是也带来了两个新的问题:用户修改操作时如何保存数据?如何清理垃圾数据?3用户修改操作如何保存数据?讲了先写MongoDB,再写数据库。这个方案中的流程图其实主要讲的是添加数据的场景。但是如果用户在操作中修改了数据,那么用户是先修改MongoDB文档中的数据,然后再修改数据库表中的数据。流程图如下:如果在MongoDB文档中修改数据成功,但是在数据库表中修改数据失败,是不是有问题?那么,用户修改操作时的数据如何保存呢?这就需要调整流程。修改MongoDB文档时,新增一条数据,而不是直接修改生成新的mongoid。然后在修改数据库表中的数据时,同时更新mongoid字段为这个新值。流程图如下:这样,如果MongoDB文档中的数据添加成功,但是数据库表中的数据修改失败,没有关系,因为数据库中的旧数据是用旧的mongoid。通过这个id,仍然可以从MongoDB文档中查询到数据。使用这种方案可以解决修改数据时的数据一致性问题,但是也会出现垃圾数据。其实这个垃圾数据是可以马上删除的。具体流程图如下:前面的过程中,修改数据库,将mongoid更新为新值后,直接删除了MongoDB文档中的旧数据。这个方案可以解决用户修改操作中99%的垃圾数据,但是还有1%的情况,就是最后删除失败怎么办?答:这个需要重试机制。我们可以使用job或者mq来重试,推荐使用mq增加重试功能。特别是对于RocketMQ,它有自己的失败重试机制和特殊的重试队列。我们可以设置重试次数。流程图优化如下:将删除MongoDB文档中数据的操作改为发送mq消息。有专门的mq消费者负责删除数据,可以作为共享函数使用。它包括一个失败重试机制。如果删除失败5次,消息将保存在死信队列中。然后有一个专门的程序来监控死信队列中的数据,如果发现有数据就会发送报警邮件。这样就可以基本解决无法修改和删除垃圾数据的问题。4如何清理新添加的垃圾数据?还有一种没有处理的垃圾数据,就是用户添加数据的时候,如果MongoDB文档写入成功,但是数据库表失败了。由于MongoDB不会回滚数据,此时MongoDB文档会保存垃圾数据,那么如何清理这类数据呢?4.1定时删除我们可以使用job定时扫描,例如:每天扫描一次MongoDB文档,取出mongoid,查询数据库中的数据,如果能找到数据,则将数据保留在MongoDB文档中.如果数据库中不存在mongoid,则删除MongoDB文档中的数据。如果MongoDB文档中的数据量不大,可以这样处理。但是如果数据量太大,这种处理就会出现性能问题。这需要优化,一种常见的方法是缩小扫描数据的范围。例如:在扫描MongoDB文档数据时,根据创建时间,只查看最近24小时的数据,查到后使用mongoid查询数据库中的数据。如果直接查看最近24小时的数据,就会出现问题。刚写入MongoDB文档,还没有写入数据库的数据也会被检出。此类数据可能会被误删除。可以将时间整体提前一个小时,例如:in_time
