36讲什么时候需要分表分库 - 图136讲什么时候需要分表分库

你好,我是刘超。

36讲什么时候需要分表分库 - 图2在当今互联⽹时代,海量数据基本上是每⼀个成熟产品的共性,特别是在移动互联⽹产品中,⼏乎每天都在产⽣数据,例如, 商城的订单表、⽀付系统的交易明细以及游戏中的战报等等。

对于⼀个⽇活⽤户在百万数量级的商城来说,每天产⽣的订单数量可能在百万级,特别在⼀些活动促销期间,甚⾄上千万。

假设我们基于单表来实现,每天产⽣上百万的数据量,不到⼀个⽉的时间就要承受上亿的数据,这时单表的性能将会严重下降。因为MySQL在InnoDB存储引擎下创建的索引都是基于B+树实现的,所以查询时的I/O次数很⼤程度取决于树的⾼度,随着B+树的树⾼增⾼,I/O次数增加,查询性能也就越差。

当我们⾯对⼀张海量数据的表时,通常有分区、NoSQL存储、分表分库等优化⽅案。

分区的底层虽然也是基于分表的原理实现的,即有多个底层表实现,但分区依然是在单库下进⾏的,在⼀些需要提⾼并发的场景中的优化空间⾮常有限,且⼀个表最多只能⽀持1024个分区。⾯对⽇益增⻓的海量数据,优化存储能⼒有限。不过在⼀些
⾮海量数据的⼤表中,我们可以考虑使⽤分区来优化表性能。

分区表是由多个相关的底层表实现的,这些底层表也是由句柄对象表示,所以我们也可以直接访问各个分区,存储引擎管理
分区的各个底层表和管理普通表⼀样(所有的底层表都必须使⽤相同的存储引擎),分区表的索引只是在各个底层表上各⾃加上⼀个相同的索引,从存储引擎的⻆度来看,底层表和⼀个普通表没有任何不同,存储引擎也⽆须知道这是⼀个普通表, 还是⼀个分区表的⼀部分。

⽽NoSQL存储是基于键值对存储,虽然查询性能⾮常⾼,但在⼀些⽅⾯仍然存在短板。例如,不是关系型数据库,不⽀持事
务以及稳定性⽅⾯相对RDBMS差⼀些。虽然有些NoSQL数据库也实现了事务,宣传具有可靠的稳定性,但⽬前NoSQL还是主要⽤作辅助存储。

什么时候要分表分库?

分析完了分区、NoSQL存储优化的应⽤,接下来我们就看看这讲的重头戏——分表分库。

在我看来,能不分表分库就不要分表分库。在单表的情况下,当业务正常时,我们使⽤单表即可,⽽当业务出现了性能瓶颈时,我们⾸先考虑⽤分区的⽅式来优化,如果分区优化之后仍然存在后遗症,此时我们再来考虑分表分库。

我们知道,如果在单表单库的情况下,当数据库表的数据量逐渐累积到⼀定的数量时(5000W⾏或100G以上),操作数据库的性能会出现明显下降,即使我们使⽤索引优化或读写库分离,性能依然存在瓶颈。此时,如果每⽇数据增⻓量⾮常⼤,我们就应该考虑分表,避免单表数据量过⼤,造成数据库操作性能下降。

⾯对海量数据,除了单表的性能⽐较差以外,我们在单表单库的情况下,数据库连接数、磁盘I/O以及⽹络吞吐等资源都是有 限的,并发能⼒也是有限的。所以,在⼀些⼤数据量且⾼并发的业务场景中,我们就需要考虑分表分库来提升数据库的并发处理能⼒,从⽽提升应⽤的整体性能。

如何分表分库?

通常,分表分库分为垂直切分和⽔平切分两种。

垂直分库是指根据业务来分库,不同的业务使⽤不同的数据库。例如,订单和消费券在抢购业务中都存在着⾼并发,如果同时使⽤⼀个库,会占⽤⼀定的连接数,所以我们可以将数据库分为订单库和促销活动库。

⽽垂直分表则是指根据⼀张表中的字段,将⼀张表划分为两张表,其规则就是将⼀些不经常使⽤的字段拆分到另⼀张表中。例如,⼀张订单详情表有⼀百多个字段,显然这张表的字段太多了,⼀⽅⾯不⽅便我们开发维护,另⼀⽅⾯还可能引起跨⻚问 题。这时我们就可以拆分该表字段,解决上述两个问题。

⽔平分表则是将表中的某⼀列作为切分的条件,按照某种规则(Range或Hash取模)来切分为更⼩的表。

⽔平分表只是在⼀个库中,如果存在连接数、I/O读写以及⽹络吞吐等瓶颈,我们就需要考虑将⽔平切换的表分布到不同机器的库中,这就是⽔平分库分表了。

结合以上垂直切分和⽔平切分,我们⼀般可以将数据库分为:单库单表-单库多表-多库多表。在平时的业务开发中,我们应该优先考虑单库单表;如果数据量⽐较⼤,且热点数据⽐较集中、历史数据很少访问,我们可以考虑表分区;如果访问热点数据分散,基本上所有的数据都会访问到,我们可以考虑单库多表;如果并发量⽐较⾼、海量数据以及每⽇新增数据量巨⼤,我们可以考虑多库多表。

这⾥还需要注意⼀点,我刚刚强调过,能不分表分库,就不要分表分库。这是因为⼀旦分表,我们可能会涉及到多表的分⻚查询、多表的JOIN查询,从⽽增加业务的复杂度。⽽⼀旦分库了,除了跨库分⻚查询、跨库JOIN查询,还会存在跨库事务的问 题。这些问题⽆疑会增加我们系统开发的复杂度。

分表分库之后⾯临的问题

然⽽,分表分库虽然存在着各种各样的问题,但在⼀些海量数据、⾼并发的业务中,分表分库仍是最常⽤的优化⼿段。所以, 我们应该充分考虑分表分库操作后所⾯临的⼀些问题,接下我们就⼀起看看都有哪些应对之策。

为了更容易理解这些问题,我们将对⼀个订单表进⾏分库分表,通过详细的业务来分析这些问题。

假设我们有⼀张订单表以及⼀张订单详情表,每天的数据增⻓量在60W单,平时还会有⼀些促销类活动,订单增⻓量在千万 单。为了提⾼系统的并发能⼒,我们考虑将订单表和订单详情表做分库分表。除了分表,因为⽤户⼀般查询的是最近的订单信息,所以热点数据⽐较集中,我们还可以考虑⽤表分区来优化单表查询。

通常订单的分库分表要么基于订单号Hash取模实现,要么根据⽤户 ID Hash 取模实现。订单号Hash取模的好处是数据能均匀分布到各个表中,⽽缺陷则是⼀个⽤户查询所有订单时,需要去多个表中查询。

由于订单表⽤户查询⽐较多,此时我们应该考虑使⽤⽤户ID字段做Hash取模,对订单表进⾏⽔平分表。如果需要考虑⾼并发
时的订单处理能⼒,我们可以考虑基于⽤户ID字段Hash取模实现分库分表。这也是⼤部分公司对订单表分库分表的处理⽅式。

1.分布式事务问题

在提交订单时,除了创建订单之外,我们还需要扣除相应的库存。⽽订单表和库存表由于垂直分库,位于不同的库中,这时我们需要通过分布式事务来保证提交订单时的事务完整性。

通常,我们解决分布式事务有两种通⽤的⽅式:两阶事务提交(2PC)以及补偿事务提交(TCC)。有关分布式事务的内容, 我将在第41讲中详细介绍。

通常有⼀些中间件已经帮我们封装好了这两种⽅式的实现,例如Spring实现的JTA,⽬前阿⾥开源的分布式事务中间件
Fescar,就很好地实现了与Dubbo的兼容。

2.跨节点JOIN查询问题

⽤户在查询订单时,我们往往需要通过表连接获取到商品信息,⽽商品信息表可能在另外⼀个库中,这就涉及到了跨库JOIN 查询。

通常,我们会冗余表或冗余字段来优化跨库JOIN查询。对于⼀些基础表,例如商品信息表,我们可以在每⼀个订单分库中复制⼀张基础表,避免跨库JOIN查询。⽽对于⼀两个字段的查询,我们也可以将少量字段冗余在表中,从⽽避免JOIN查询,也就避免了跨库JOIN查询。

3.跨节点分⻚查询问题

我们知道,当⽤户在订单列表中查询所有订单时,可以通过⽤户ID的Hash值来快速查询到订单信息,⽽运营⼈员在后台对订 单表进⾏查询时,则是通过订单付款时间来进⾏查询的,这些数据都分布在不同的库以及表中,此时就存在⼀个跨节点分⻚查询的问题了。

通常⼀些中间件是通过在每个表中先查询出⼀定的数据,然后在缓存中排序后,获取到对应的分⻚数据。这种⽅式在越往后⾯的查询,就越消耗性能。

通常我们建议使⽤两套数据来解决跨节点分⻚查询问题,⼀套是基于分库分表的⽤户单条或多条查询数据,⼀套则是基于
Elasticsearch、Solr存储的订单数据,主要⽤于运营⼈员根据其它字段进⾏分⻚查询。为了不影响提交订单的业务性能,我们
⼀般使⽤异步消息来实现Elasticsearch、Solr订单数据的新增和修改。

4.全局主键ID问题

在分库分表后,主键将⽆法使⽤⾃增⻓来实现了,在不同的表中我们需要统⼀全局主键ID。因此,我们需要单独设计全局主键,避免不同表和库中的主键重复问题。

使⽤UUID实现全局ID是最⽅便快捷的⽅式,即随机⽣成⼀个32位16进制数字,这种⽅式可以保证⼀个UUID的唯⼀性,⽔平扩展能⼒以及性能都⽐较⾼。但使⽤UUID最⼤的缺陷就是,它是⼀个⽐较⻓的字符串,连续性差,如果作为主键使⽤,性能相对来说会⽐较差。

我们也可以基于Redis分布式锁实现⼀个递增的主键ID,这种⽅式可以保证主键是⼀个整数且有⼀定的连续性,但分布式锁存在⼀定的性能消耗。

我们还可以基于Twitter开源的分布式ID⽣产算法——snowflake解决全局主键ID问题,snowflake是通过分别截取时间、机器标识、顺序计数的位数组成⼀个long类型的主键ID。这种算法可以满⾜每秒上万个全局ID⽣成,不仅性能好,⽽且低延时。

5.扩容问题

随着⽤户的订单量增加,根据⽤户 ID Hash 取模的分表中,数据量也在逐渐累积。此时,我们需要考虑动态增加表,⼀旦动
态增加表了,就会涉及到数据迁移问题。

我们在最开始设计表数据量时,尽量使⽤2的倍数来设置表数量。当我们需要扩容时,也同样按照2的倍数来扩容,这种⽅式可以减少数据的迁移量。

总结

在业务开发之前,我们⾸先要根据⾃⼰的业务需求来设计表。考虑到⼀开始的业务发展⽐较平缓,且开发周期⽐较短,因此在开发时间⽐较紧的情况下,我们尽量不要考虑分表分库。但是我们可以将分表分库的业务接⼝预留,提前考虑后期分表分库的切分规则,把该冗余的字段提前冗余出来,避免后期分表分库的JOIN查询等。

当业务发展⽐较迅速的时候,我们就要评估分表分库的必要性了。⼀旦需要分表分库,就要结合业务提前规划切分规则,尽量避免消耗性能的跨表跨库JOIN查询、分⻚查询以及跨库事务等操作。

思考题

你使⽤过哪些分库分表中间件呢?欢迎分享其中的实现原理以及优缺点。

期待在留⾔区看到你的分享。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他⼀起讨论。
36讲什么时候需要分表分库 - 图3

  1. 精选留⾔ <br />![](https://cdn.nlark.com/yuque/0/2022/png/1852637/1646315691759-aa1920a8-9e60-4053-8987-aadb50c4fa7a.png#)QQ怪<br />没有什么⼤⼚经验,看了⽼师的分享的确对⼤⼚数据库分库分表设计有⼀定的理解和提⾼<br />2019-08-15 09:24<br />![](https://cdn.nlark.com/yuque/0/2022/png/1852637/1646315692392-8dcad50d-a631-4c42-8f58-bcafdf1bba65.png#)QQ怪<br />现在Fescar已经改名为Seata<br />2019-08-15 09:16<br />作者回复<br />看来成⻓⽐较迅速呀。这个开源中间件⽐较新,去年刚开源的时候了解下源码,顺便实践了基本功能。<br />2019-08-16 09:35

36讲什么时候需要分表分库 - 图4梦醒时分
⾃⼰调研过Mycat,个⼈感觉挺好⽤的。⽣产上没⽤过,不知道稳定性怎么样
2019-08-16 10:30
36讲什么时候需要分表分库 - 图5许童童
我们的系统前期没设计好,现在想分库分表很难,⼤量join查询,很难处理。想⽤阿⾥云的分⻚式rdbms,不知道可⾏性。
2019-08-15 14:42
作者回复
前期没有做好分表的准备,后⾯做表升级⼯作量就⼤很多,⽽⻛险更⾼。

例如⼀个表如果是⾃增主键ID,⽽主键ID⼜跟其他业务表做了耦合,当我们要做表升级时,需要⽤另外⼀个字段做分表字段, 这时候就存在主键ID在分表后可能存在冲突的问题。 所以⼀开始我们就要想到这张表有可能需要做表升级,在做表关联时⽤另外⼀个⾮⾃增主键ID做关联,或者使⽤全局⾃增ID或雪花算法统⼀获取全局主键ID。

阿⾥云的数据库暂时没有⽤过,多了解⽀持的⼀些功能,匹配下是否更适合⾃⼰的业务。
2019-08-16 09:30

36讲什么时候需要分表分库 - 图6咬尖⽉⽛⼉
⽼师,可以⽤tidb这种newsql取代分库分表的⽅案吗
2019-08-15 09:33
作者回复
可以的,TiDB是⼀种集中式的数据存放解决⽅案,可以节省开发⼈员很多⼯作量。
2019-08-16 09:55

36讲什么时候需要分表分库 - 图7明天更美好
个⼈感觉单表超过500w就要分表,不然对于性能有要求的业务来说性能太差了。单库数据超2T,就得分库。这样的话可能更合理些
2019-08-15 08:58
36讲什么时候需要分表分库 - 图8明天更美好
有个问题请教下,对于分库分表来说主键需要严格递增,不然数据会发⽣倾斜的,造成部分库数据过多。雪花算法虽然好,但是只是趋势递增的,那怎么才能做到严格递增呢?并且对性能上有⼀定要求。
2019-08-15 08:56
作者回复
正如⽂中提到的,可以通过数据库或缓存来实现严格的递增,但会有⼀定的性能消耗。⻥和熊掌两者不可兼得,⽬前雪花算法是⼀种折中解决⽅案。
2019-08-15 09:18

⻔窗⼩⼆
⽤了sharding jdbc ,但实际实现的时候尽量还是考虑单库
2019-08-15 07:50
36讲什么时候需要分表分库 - 图9失⽕的夏天
⽤过阿⾥的DRDS,它只⽀持⼀个字段作为分库分表键,不能多个字段同时分库分表,⽽且不⽀持分布式事务,如果要修改分库键的值,就要先插⼊再删除,或者先删除再插⼊。插⼊,更新,查询的时候都要带上分库键。不过好像有个参数可以控制是不是⼀定要带分库键,⼤概就了解这么些。
2019-08-15 07:13
36讲什么时候需要分表分库 - 图10胡峣
⽤了段时间shardingjdbc,最后还是回归了单服务单库
2019-08-15 00:36