• 什么时候需要分表分库?


    在当今互联网时代,海量数据基本上是每一个成熟产品的共性,特别是在移动互联网产品中,几乎每天都在产生数据,例如,商城的订单表、支付系统的交易明细以及游戏中的战报等等。
    对于一个日活用户在百万数量级的商城来说,每天产生的订单数量可能在百万级,特别在一些活动促销期间,甚至上千万。
    假设我们基于单表来实现,每天产生上百万的数据量,不到一个月的时间就要承受上亿的数据,这时单表的性能将会严重下降。因为MySQL在InnoDB存储引擎下创建的索引都是基于B+树实现的,所以查询时的I/O次数很大程度取决于树的高度,随着B+树的树高增高,I/O次数增加,查询性能也就越差。
    当我们面对一张海量数据的表时,通常有分区、NoSQL存储、分表分库等优化方案。

    一 什么时候要分表分库?

    在我看来,能不分表分库就不要分表分库。在单表的情况下,当业务正常时,我们使用单表即可,而当业务出现了性能瓶颈时,我们首先考虑用分区的方式来优化,如果分区优化之后仍然存在后遗症,此时我们再来考虑分表分库。
    我们知道,如果在单表单库的情况下,当数据库表的数据量逐渐累积到一定的数量时(5000W行或100G以上),操作数据库的性能会出现明显下降,即使我们使用索引优化或读写库分离,性能依然存在瓶颈。此时,如果每日数据增长量非常大,我们就应该考虑分表,避免单表数据量过大,造成数据库操作性能下降。

    二 如何分表分库?

    通常,分表分库分为垂直切分和水平切分两种。
    垂直分库是指根据业务来分库,不同的业务使用不同的数据库。例如,订单和消费券在抢购业务中都存在着高并发,如果同时使用一个库,会占用一定的连接数,所以我们可以将数据库分为订单库和促销活动库。
    而垂直分表则是指根据一张表中的字段,将一张表划分为两张表,其规则就是将一些不经常使用的字段拆分到另一张表中。例如,一张订单详情表有一百多个字段,显然这张表的字段太多了,一方面不方便我们开发维护,另一方面还可能引起跨页问题。这时我们就可以拆分该表字段,解决上述两个问题。
    水平分表则是将表中的某一列作为切分的条件,按照某种规则(Range或Hash取模)来切分为更小的表。
    水平分表只是在一个库中,如果存在连接数、I/O读写以及网络吞吐等瓶颈,我们就需要考虑将水平切换的表分布到不同机器的库中,这就是水平分库分表了。

    三 分表分库之后面临的问题

    然而,分表分库虽然存在着各种各样的问题,但在一些海量数据、高并发的业务中,分表分库仍是最常用的优化手段。所以,我们应该充分考虑分表分库操作后所面临的一些问题,接下我们就一起看看都有哪些应对之策。
    为了更容易理解这些问题,我们将对一个订单表进行分库分表,通过详细的业务来分析这些问题。
    假设我们有一张订单表以及一张订单详情表,每天的数据增长量在60W单,平时还会有一些促销类活动,订单增长量在千万单。为了提高系统的并发能力,我们考虑将订单表和订单详情表做分库分表。除了分表,因为用户一般查询的是最近的订单信息,所以热点数据比较集中,我们还可以考虑用表分区来优化单表查询。
    通常订单的分库分表要么基于订单号Hash取模实现,要么根据用户 ID Hash 取模实现。订单号Hash取模的好处是数据能均匀分布到各个表中,而缺陷则是一个用户查询所有订单时,需要去多个表中查询。
    由于订单表用户查询比较多,此时我们应该考虑使用用户ID字段做Hash取模,对订单表进行水平分表。如果需要考虑高并发时的订单处理能力,我们可以考虑基于用户ID字段Hash取模实现分库分表。这也是大部分公司对订单表分库分表的处理方式。

    1.分布式事务问题

    在提交订单时,除了创建订单之外,我们还需要扣除相应的库存。而订单表和库存表由于垂直分库,位于不同的库中,这时我们需要通过分布式事务来保证提交订单时的事务完整性。
    通常,我们解决分布式事务有两种通用的方式:两阶事务提交(2PC)以及补偿事务提交(TCC)。有关分布式事务的内容,我将在后面的讲解中详细介绍。

    2.跨节点JOIN查询问题

    用户在查询订单时,我们往往需要通过表连接获取到商品信息,而商品信息表可能在另外一个库中,这就涉及到了跨库JOIN查询。
    通常,我们会冗余表或冗余字段来优化跨库JOIN查询。对于一些基础表,例如商品信息表,我们可以在每一个订单分库中复制一张基础表,避免跨库JOIN查询。而对于一两个字段的查询,我们也可以将少量字段冗余在表中,从而避免JOIN查询,也就避免了跨库JOIN查询。

    3.跨节点分页查询问题

    我们知道,当用户在订单列表中查询所有订单时,可以通过用户ID的Hash值来快速查询到订单信息,而运营人员在后台对订单表进行查询时,则是通过订单付款时间来进行查询的,这些数据都分布在不同的库以及表中,此时就存在一个跨节点分页查询的问题了。
    通常一些中间件是通过在每个表中先查询出一定的数据,然后在缓存中排序后,获取到对应的分页数据。这种方式在越往后面的查询,就越消耗性能。

    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查询、分页查询以及跨库事务等操作。

  • 相关阅读:
    阿里巴巴专场——第322场周赛题解
    C语言入门Day_20 函数的定义
    python系列教程191——nonlocal边界
    属性和字段的区别
    面试官:能用JavaScript手写一个bind函数吗
    系列文章|云原生时代下微服务架构进阶之路 - Spring Boot
    Java 线程池
    Spring学习笔记(三十七)——Flyway 数据库版本控制
    Apache-tomcat-8.5.82下载安装以及环境变量配置
    杀死僵尸进程ZooKeeperMain
  • 原文地址:https://blog.csdn.net/LBWNB_Java/article/details/126865072