分库分表

不使用 MySQL 的分区表,而是自己实现。

业务分库

业务分库带来了新的问题

1. join 操作问题

业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询。

2. 事务问题

原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。

3. 成本问题

需要更多的数据库。

分表

将不同业务数据分散存储到不同的数据库中,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库的处理瓶颈。此时就需要对单表数据进行拆分。

单表数据拆分有两种方式:垂直分表水平分表

单表进行切分后,可以根据实际的切分效果来确定,是否要将切分后的多个表分散在不同的数据库中,并不一定要将单表切分为多表后分散到不同数据库中,可以根据未来数据的规模以及技术因素来确定。

垂直分表

垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。

垂直分表引入的复杂性主要体现在表操作的数量要增加。例如,原来只要一次查询就可以获取 name、age、sex、nickname、avatar,现在需要两次查询,一次查询获取 name、age、sex,另外一次查询获取 nickname、avatar。

水平分表

水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:

路由

水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。

常见的路由算法有:

  • 范围路由

选取有序的数据列(例如,整形、时间戳等)作为路由的条件,不同分段分散到不同的数据库表中。以最常见的用户 ID 为例,路由算法可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到数据库 1 的表中,1000000 ~ 1999999 放到数据库 2 的表中,以此类推。

范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适的分段大小。

范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。

范围路由的一个比较隐含的缺点是分布不均匀,假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1000 条,而另外一个分段实际存储的数据量有 900 万条。

  • 取模路由

选取某个列(或者某几个列组合也可以)的值进行取模 运算,一般为表的唯一 ID,然后根据取模结果分散到不同的数据库表中。

以用户 ID 为例,假如我们一开始就规划了 10 个数据表,路由算法可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号为 6 的字表中。

用这种方式分表时,唯一 ID 的生成不会再通过数据库的自增产生,因为我们要保证 ID 的唯一,又要保证生成的 ID 尽可能的平均分散在各个表里。目前有一些开源的算法可以生成唯一的 ID,如:Twitter 的 Snowflake idworker 算法。

取模路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了取模路由后,增加字表数量是非常麻烦的,所有数据都要重分布。

  • 配置路由

配置路由就是路由表,用一张独立的表来记录路由信息。同样以用户 ID 为例,我们新增一张 user_router 表,这个表包含 user_id 和 table_id 两列,根据 user_id 就可以查询对应的 table_id。

配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。

配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据),性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。

分表后带来的问题

  • join 操作

水平分表后,数据分散在多个表中,如果需要与其他表进行 join 查询,需要在业务代码或者数据库中间件中进行多次 join 查询,然后将结果合并。

  • order by 操作

水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。

实现方法

和数据库读写分离类似,分库分表具体的实现方式也是「程序代码封装」和「中间件封装」,但实现会更复杂。

分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等,然后再根据不同的操作进行不同的处理。例如 order by 操作,需要先从多个库查询到各个库的数据,然后再重新 order by 才能得到最终的结果。