为什么要分?

分库的目的是做“业务拆分”,通过业务拆分,把一个大的复杂系统拆成多个业务子系统,之间通过 RPC 或消息中间件通信。这样做既便于团队成员的职责分工,也便于对未来某个系统进行扩展。

第二个考虑是应对高并发。但要针对读多写少,还是读少写多的场景分别讨论。如果是读多写少,可以通过加从库、加缓存解决,不一定要分库分表。如果是读少写多,或者说写入的 QPS 已经达到了数据库的瓶颈,这时就要考虑分库分表了。

另外一个考虑角度是“数据隔离”。如果把核心业务数据和非核心业务数据放在一个库里,不分轻重,同等对待。一旦因为非核心业务导致数据库宕机,核心业务也会受到牵连。分开之后,区别对待,投入的开发和运维人力也不同。

分布式 ID 生成服务

在分库之前,数据库的自增主键可以唯一标识一条记录,在分库分表之后,需要一个全局的 ID 生成服务。开源的方案有 Twitter 的 Snowflake,各大公司往往也都有自己的分布式 ID 生成服务。生成的 ID 是完全无序,还是趋势递增,或者呈更严格的单调递增,方案也不尽相同,此处不再展开讨论。可以参考:分布式 ID 实现方案

拆分维度的选择

有了全局的 ID,接下来的问题是按哪个维度进行拆分。比如电商的订单表,至少有三个查询维度:订单 ID、用户 ID、商户 ID。当拆分的时候,根据哪个维度进行拆分呢?

假设按用户 ID 维度拆分,同一个用户 ID 的所有订单会落到同一个库的同一张表里。当查询的时候,按用户 ID 查,可以很容易地定位到某个库的某个表;但如果按订单 ID 或商户 ID 维度查询,就很难做。

对于在分库分表之后其他维度的查询,一般有以下几个方法:

  1. 建立一个映射表
    建立辅助维度和主维度之间的映射关系(商户 ID 和用户 ID 之间的映射关系)。查询的时候根据商户 ID 查询映射表,得到用户 ID;再根据用户 ID 查询订单 ID。但这里有个问题:映射表本身也需要分库分表,并且分库分表维度和订单表的分库维度还不同。即使映射表不分库分表,写入一条订单的时候也可能需要同时写两个库,属于分布式事务问题。对于这种问题,通常也只能做一个后台任务定时比对,保证订单表和映射表的数据最终一致。

  2. 业务双写
    同一份数据,两套分库分表。一套按用户 ID 切分,一套按商户 ID 切分。同样,存在写入多个库的分布式事务问题。

  3. 异步双写
    还是两套表,只是业务单写。然后通过监听 Binlog,同步到另外一套表上。

  4. 两个维度统一到一个维度
    把订单 ID 和用户 ID 统一成一个维度,比如把用户 ID 作为订单 ID 中的某几位,这样订单 ID 中就包含了用户 ID 信息,然后按照用户 ID 分库,当按订单 ID 查询的时候,截取出用户 ID,再按用户 ID 查询;或者订单 ID 和用户 ID 中有某几位是相同的(两个 ID 都是字符串类型),用这几位作为分库维度。

Join 查询问题

分库分表之后,Join 查询就不能用了。针对这种情况,一般有下面几种解决方法:

  1. 把 Join 拆成多个单表查询,不让数据库做 Join,而是在代码层对结果进行拼装
    这种做法非常的常见,因为数据库全是单表查询,大大降低了数据库发生慢查询的概率。
  2. 做宽表,重写轻读
    很多时候会有这样的情况:需要把 Join 的结果分页,这需要利用 MySQL 本身的分页功能。对于这种不得不用 Join 的情况,可以另外做一个 Join 表,提前把结果 Join 好。这是“重写轻读”,其实也是“空间换时间”的思路。
  3. 利用搜索引擎
    对于第二种方法当中提到的场景,还可以利用类似 ES 的搜索引擎,把数据库中的数据导入搜索引擎中进行查询,从而解决 Join 问题。

分布式事务

做了分库之后,纯数据库的事务就做不了了。一般的解决办法是优化业务,避免跨库的事务,保证所有事务都落到单库中。

如果实在无法避免,需要分布式事务的解决方案。分布式事务是一个系统性的问题,后面会专门论述。