MongoDB 读写分离踩坑

sorcererxw /

发现问题与排查

这两天开发了一个后端更新用户个人信息的接口 POST /profile, 最初的设计是前端提交请求之后,后端更新信息之后,并从数据库里面读取最新的数据,返回给客户端。就是一个写+读的操作,非常简单,很快就开发完了,并通过了单元测试,上线到测试环境。

不过很快测试同学就告诉我每次更新完返回的数据都不是最新的,返回了上一次的旧数据。不应该啊,明明已经通过了单元测试。

于是开始排查,一开始是怀疑代码里面的 Promise 被异步调用了,本地单元测试执行速度过快而没有发现问题。但很快这个问题就被排除了,很明显,如果异步调用了,在 event loop 模式下根本不可能存在正确的情况。

然后开始将目光放到了项目当中使用的 ODM 上,我使用的 MongoDB ODMMongoose, 因为原生的 Mongoose 应该是对不同事件进行 callback 来执行相关逻辑的,但是切换到现在的 async/await 模式,担心 await 的结果不是真实的调用结果。或者说,怀疑 Mongoose 将数据操作发送到 MongoDB 服务器将认为请求成功,那么调用方应该是无法实时获得新的数据的。查了一圈 StackOverflow,翻了一遍文档,发现 Mongoose 的更新操作确实是同步的,一定是 MongoDB 确实写入了数据才返回结果。

最后,将目光瞄向了数据库连接配置上,最后在注意到了 MongoDB 的 url 上有一个参数 readPreference=secondaryPreferred。这下我才恍然大悟过来,原来这中间的延迟出现在 MongoDB 主从复制。这样,同样也能解释为什么本地单元测试就能通过,而线上环境就会出现问题:因为本地连接的并不是数据库集群,所有读写都存在于主库当中,这样就不会存在时延。

解决方案

既然找到问题,那么就得考虑如何解决这个问题

  • ❌ 将读操作放在主库上

    这个当然能够一步到位解决所有相关的问题,但是这样就失去了数据库读写分离的意义了。当系统中的读操作远多于写操作的时候,这样会造成系统性能降低。

    当然,也可以设置需要实时获取新数据的时候,选择从主库进行读。不过 Mongoose 不支持动态选择实例。

  • ⚠️ update 接口不实时返回

    Restful 规范里面,建议更新操作之后,返回更新后的实体。但是一方面并不是所有更新操作都需要立即获取最新的数据。另一方面,如果需要保证数据一致性,还是要以 Get 请求得到的数据为准。前端也不应该以返回的实体是否符合来确定是否更新成功,而是应该以返回的 200 状态码为准。

    当然将 update 接口设计成不返回实体,并不能完全解决主从复制时延的问题。因为系统内部,也有不少操作是将实时生成新数据,然后再从数据库进行拉取。这种情况就需要再重新设计了。

    所以这个这个方案还是陷入了头痛医头脚痛医脚的问题,不具有通用型。

  • ✅ Write Concern

    MongoDB 还有一个 Write-Concern 机制,可以根据具体使用场景,灵活调整分布式数据于读写机制。

    对于写操作,Write-Concern 提供了多个可选参数。

    对于读操作,find 操作有一些有意思的参数

    说了这么多,回到我的使用场景上,比较合适的做法应该是 {w: "majority"} + {awaitData: true}