记一次 mongodb 单表亿级数据的拆分方案

三月沙 原文链接

拆表是一种常见的解决单表数据库瓶颈的方案,在实际的应用场景中能够部分解决单表的写压力和读压力,但是也会带来一些更复杂的影响:

  • 聚合查询变得困难
  • 拆分的键一旦选定,更改会非常困难
  • 拆表的过程要保证线上业务不受影响,操作复杂度高

因此,表的拆分一定要选在恰当的时候进行,过早,付出很大代价也并不会带来性能的提升,过晚,数据量庞大,操作难度加大。

在本次实施的拆分方案中,数据特点是:

  • 单表过亿
  • 业务数据是用户对资源的收藏,结构比较单一
  • 只做单向(用户–>数据)查找,不要求反向(数据–>用户)查找
  • 数据处于实时变更状态(新增、删除、查询等操作并存)

如何选择拆分键

本例中,拆分的键就是用户 id,且每个用户关联的资源数据最大不会过万。

如何保证拆分的过程中不影响用户的操作?

本例中,表中的数据基本上只有如下操作

  • 用户新增一条数据
  • 用户删除一条数据
  • 用户查询数据(有翻页)

拆表的基本原理就是对选择的键进行 hash 生成每个键所属的表空间名,也就是说,在任意时刻,任意用户的数据只有以下三种状态

1.全部在旧表中
2.部分在新表,部分在旧表
3.全部在新表中

数据拆分的具体工作是要离线进行的,为了能保证用户数据在这样三种状态之下依然具有像单表中一样的一致性,需要业务层在处理当前用户数据时判断用户是否在迁移中,只有处于迁移状态之下,用户数据才需要特别处理。

迁移状态如何判断?

如果当前用户数据处于迁移状态,为了保证用户数据全部可用,即要做到不跨表翻页,这里做了特殊处理:一旦开始迁移,用户的全部数据就会载入到一个特殊区域(这里我们用了 redis,后面再讨论这样处理可能带来的问题),且保存为有序集数据。

所以,如果用户处于迁移状态,则用户的数据一定存在于这个区域,暂且给此区域命名为 on_progress

迁移状态下如何保证用户数据的正确性?

当用户处于迁移状态,用户新增一条数据,则在新表和 on_progress 中各写入新增数据,用户删除一条数据,则在新表、on_progress同时删除该数据。

这样做的目的是什么?

处于迁移状态下的用户,on_progress 中保存了其所有数据,可以进行翻页操作,on_progress 中的所有数据会在离线状态下不断写入新表中,迁移完成之后,旧表中的数据将被删除,on_progress 中数据也将被清除,这样用户数据全部进入新表中,后续的所有操作将只在新表上进行。

on_progress 中的数据在不断写入新表的过程中,是按照由新–>旧或由旧–>新的顺序进行,在这个过程中:

用户删除一条数据

1)有可能该条数据已经写入新表中,所以删除操作要在新表中执行。
2)on_progress 中的数据要始终和用户所有的操作同步来保证数据的正确性和一致性,因而删除操作也要在 on_progress 中执行。

用户新增一条数据

1)on_progress 中的数据要始终和用户所有的操作同步来保证数据的正确性和一致性,因而 on_progress 需要写入一条数据。
2)在某些边界条件下,on_progress (假如数据由旧–>新写入新表) 中的数据迁移完成但还未被删除,用户恰巧写入一条数据,此时用户仍处于迁移状态中,但是离线的迁移操作已经认为迁移停止了,因而此时新增数据需要进入新表才能保证数据最终是正确的。

on_progress 的选择

在这个方案的实施中,on_progress 用了 redis 的有序集,键就是用户的 id,只要检测到用户 id 的存在就认为该用户处于迁移状态中。

迁移完成之后,先删除旧表中的数据,然后再删除 on_progress 中有序集,所以,只要用户处于 redis 中,则此时一定是迁移中或迁移完成,如果在旧表中找到用户数据,则该用户一定还没有开始迁移,否则用户已经迁移成功。

需要注意的是,选用 redis 可能带来的问题是 redis 宕机会导致迁移中的数据无法回复(redis 未开持久化操作或其他原因导致数据不能恢复),只要用户在旧表中有数据存在,则用户删除或新增数据的操作一定也要在旧表中执行,再次恢复迁移时,要清除新表数据之后才能正常进行。

迁移流程图