一文了解数据拆分与分库分表

2021-07-23 16:46:50 浏览数 (1)

数据库在业务体系不大的情况,一般都是单库出现,通过增加主从复制提高SLA。但当业务体量不断扩大,就需要考虑进行数据拆分来解决性能瓶颈问题。

下面讨论的技术方案主要是从MySQL角度考虑的,其他类型数据库仅作参考。

拆分策略

水平拆分

把单表表分割N个结构相同的表,然后把数据按照策略分散到不同的表中,这是表层面的水平拆分;如果把表在分散在不同的数据库中,这是数据库层面的水平拆分。

比如:订单表每个月有百万条数据累计,每个月都可以进行水平拆分,将上个月的数据放到另外一个数据库。

水平拆分带来的问题:

  1. 跨库的关联查询性能差。
  2. 数据多次扩容和维护量大。
  3. 跨分片的事务一致性难以保证。

垂直拆分

把单表中数据按照不同特点,拆分成两张不同的表,常见的策略是根据数据是修改频繁还是读取频繁,把修改频繁的字段放一张表,读取频繁的放另一张表,这是表层面的垂直拆分;如果根据业务属性进行聚集,拆分到不同库的不同表,这是数据库层面的垂直拆分。

比如:用户信息表将用户经验、用户最后登录时间等频繁变化的字段从用户基础信息中抽离出来单独建表。

垂直拆分带来的问题:

  1. 依然存在单表数据量过大的问题。
  2. 部分表无法关联查询,只能通过接口聚合方式解决,增加了开发的复杂度。
  3. 分布式事务处理复杂。

拆分方式

读写分离

读写分离是数据库拆分的最基本方式,实现起来难度也不大,只需要根据读写库的配置,把业务中数据写操作路由到写库(主库),数据读操作路由到读库(从库)即可。

分库分表

分库分表主要用来解决单表数据量过大的问题,根据特定字段的路由规则,把数据分散到不同的库,不同的表中。

分库分表的作用:

分库:单一数据库支持的读/写QPS是有限的,可以将一个数据库的数据拆分到多个库中,解决数据库连接数不足的问题。

从运维成本方面考虑,当一个数据库实例中的数据过大,对于DBA的运维压力就会增大。数据备份和恢复的时间成本都将随着数据量的大小而愈发不可控。一般来讲,单一数据库实例数据的阈值在1TB之内,是比较合理的范围。

分表:单一张表的数据量过大,即使用索引来查询数据延迟也会变高,所以可以将一张表的数据拆分到多张表,查询时,只用查拆分后的某一张表,SQL语句的查询性能得到提升。

MySQL数据库大多采用B 树类型的索引,在单表数据量过大的情况下,索引深度的增加也使得磁盘访问的IO次数增加,进而导致查询性能的下降。

业务分库

基于业务特点拆分数据库,是微服务架构的基础用法。不同的业务数据隔离开来,避免单点故障导致全系统的数据库宕机。

这种模式虽然看起来更合理,但是却引入了接口幂等性和分布式事务等一系列问题。

用户分库

对于tob业务,在多租户场景下会根据客户流水大小提供不相同的服务和数据库。

此外也可能根据客户其他信息进行策略,比如地区,常见云服务的应用,选择华南,华北,华东区之类的。

分库分表

分库分表实属无奈之举,如果数据量低于百万,无需分库分表。如果单表数据量大的话,可以先考虑将数据存到ElasticSearch中,直接查询ES。或者先查询ES,然后通过主键再去查MySQL。

分库分表引入的问题

  1. 分布式事务问题

做了垂直分库或者水平分库以后,就必然需要考虑跨库执行SQL带来的分布式事务问题。

有关分布式事务的知识,可以查看博客《一文理解分布式事务的解决方案》

  1. 跨库join的问题

分库分表后,表之间的关联操作将受到限制,就无法join位于不同分库的表,也无法join分表粒度不同的表,

解决方法:

  • 字段冗余。把需要join的字段冗余在各个表中。
  • 系统层组装。业务层分别查询出所有符合条件的数据,然后在业务层组装起来,类似于一个MapReduce的过程。
  • 全局表。系统中所有模块都可能依赖的一些表,为了避免跨库join查询,可以将这类表在每个数据库中都保存一份。但要求这些数据很少进行修改或者不被修改,减少一致性问题。
  1. 横向扩容的问题

当使用哈希取模做分表的时候,针对数据量的递增需要动态的增加表,此时就需要考虑数据迁移的问题。

如果原本8个表承载量不够需要扩展到16个表的时候,hash对16取模,新数据是没有问题的,但旧数据会查询不到。

解决方法:

  • 对旧数据进行数据迁移。

迁移数据的过程可以参考Redis的底层结构字典的扩容(渐进式rehash),可以查看博客《一文理解Redis底层数据结构》

  1. 结果集合并、排序的问题

因为数据分散存储到不同的库表中,当查询指定数据列表时,数据来源于不同的子库或者子表,就必然会引发结果集合并、排序的问题。如果每次查询都需要排序、合并等操作,性能肯定会受非常大的影响。

解决方法:

  • 结合业务处理,在需求评审上做权衡。
  • 做好查询的缓存机制。

分库分表的方式

  1. 按租户来分。对于多租户场景,可以考虑。
  2. 按range来分。例如按照时间范围来分。但这种方式需要考虑数据热点问题,例如大量的流量都打在最新的库表上。
  3. 按分表键hash值来分,保证数据分布均匀分散。但这种方式扩容需要进行迁移操作。

保证分库分表之后主键ID的唯一性

在单库环境下,单表的ID采用MySQL自增的方式。但是,分库之后如果还继续使用数据库自增的方式,就会出现的主键ID重复问题。

这时候就需要一个稳定高性能的分布式ID生成策略,可以查看博客《一文了解分布式系统ID生成策略》

分库分表基于非分表键的查询

通常情况下,分库分表的时候,分区字段只有一个。例如对于用户表user,按照user_id字段进行分区,那么之后查询某个用户的信息,只能根据user_id作为分区字段。查询使用其他字段(例如手机号phone_id),则需要扫描所有分表,效率很低。

解决方法:

异构索引

针对这类场景问题,最常用的是采用“异构索引表”的方式解决,即采用异步机制将原表的每一次创建或更新,都换另一个维度保存一份完整的数据表或索引表,拿空间换时间。

需要注意的是,对于更新操作,只能操作原表;异构索引表只能执行查询操作。原表的增量数据,实时地通过同步组件,同步到异构索引表中。

分库分表的技术选型

MySQL自带分区机制,但大部分互联网公司还是愿意选择Sharding-JDBC、MyCat或者自研中间件,原因主要是第三方的分库分表中间件可以更加灵活掌控业务场景与访问模式。

Sharding-JDBC

Sharding-JDBC定位为轻量级Java框架,是ShardingSphere生态圈中的一个独立产品。更多介绍可以查看参考文档1。

Sharding-JDBC这种client层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高。但是如果需要升级,则需要各个系统都进行部署或者做版本隔离:各个系统耦合sharding-jdbc的依赖。

MyCat

MyCat网上资料很多,不过多作讲解。

需要注意的是MyCat这种proxy层方案的缺点在于需要部署并运维一套中间件,运维成本高,但好处在于对于各个项目是透明的,如果遇到升级这种操作只需要中间件那里修改就可以了。

总结

如果没有自研投入的话,分库分表组件选择Sharding-JDBC、MyCat都是不错的选择。

个人建议中小型公司选用Sharding-JDBC,client层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些;但是中大型公司最好还是选用MyCat这类proxy层方案(最好是二次开发或自研),因为大公司系统和项目非常多,人员充足,那么最好有中台部门来研究和维护分库分表组件,然后业务部门直接使用即可。

如果是Service Mesh架构的话,可以期待下ShardingSphere-Sidecar,其优势在于对Kubernetes和Mesos的云原生支持。相信数据网格会给网格架构带来更多期待。

参考文档:

  1. https://shardingsphere.apache.org/document/current/cn/overview/

0 人点赞