MySQL 分库分表策略

MySQL 分库分表

在使用关系型数据库过程中,随着用户数据不断增大,单表的数据达到1000W或100G以后,索引膨胀,单机的水平已经很难满足需求(存储容量、连接数、处理能力都有限),这个时候即时使用主从复制读写分离、优化查询、优化索引性能仍然达不到要求,此时就要考虑数据库的切分(Sharding),切分的目的就在于减少数据库的负担,缩短查询时间。

数据切分根据其切分类型,可以分为两种切分方式:垂直切分和水平切分。

垂直切分

垂直切分又分为垂直分库和垂直分表

垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与”微服务治理”的做法相似,每个微服务使用单独的一个数据库。(例如 如用户User一个库,商品Producet一个库,订单Order一个库)

垂直分库.png

垂直分表是针对列进行的。如果某个表的字段较多,可以把不常用的字段或者长度较长的字段拆分到一张新的扩展表中。在字段较多的情况下,通过“大表拆小表”,更有利于维护与开发,也能避免跨页问题(一致性、排序等问题)。MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。

垂直分表.png

垂直切分的优点

  • 降低业务系统层面的耦合
  • 通过对不同业务的分级管理与维护,业务清晰
  • 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈

垂直切分的缺点

  • 分布式系统的一致性问题
  • 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度
  • 垂直切分并没有解决单表数据量过大的问题(1000W行切分后还是1000W行)

水平切分

当垂直切分还是无法满足需求的时候(数据量行数巨大,存在单库读写、存储性能),这时候就要进行水平切分

水平切分又分为库内分表与分库分表,是根据表内数据内在的逻辑关系(可以使顺序关系、地理关系、时间关系或者是hash),将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。

水平切分.png

库内分表是将表中的数据拆分成多个表,但是这些表仍然在同一个库中,虽然能解决单表数据量过大的问题,但是仍然在一个库中就意味着仍然有IO瓶颈,大家还是竞争同一个物理机的CPU、内存、网络IO。最好通过分库分表来解决。

水平切分的优点

  • 不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力
  • 应用端改造较小,不需要拆分业务模块

    水平切分的缺点

  • 跨分片的事务一致性难以保证
  • 跨库的join关联查询性能较差
  • 数据多次扩展难度和维护量极大

水平切分的几种分片方式

  1. RANGE 切分

    例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId为1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推。以此类推。某种意义上,某些系统中使用的”冷热数据分离”,将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。这样可以做到单表大小可控、同时由于是顺序存储,在扩展结点上是非常有利的,无需进行数据迁移,另外使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题,但是这种方式有个缺点就是数据的访问量不均匀,比如热数据一致面临着高请求以及频繁的读写但是冷数据却很少被访问。

范围.png

  1. hash取模

    一般采用hash取模mod的切分方式。这种方法解决了数据访问不均匀的问题,但是在集群扩容的时候,数据迁移量是很大的(可以使用一致性hash来减缓数据迁移),另外容易面临跨分片查询的复杂问题。比如查询一个字段的时候,很难定位到某个数据库,此时要对所有集群进行查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。

hash.png

分库分表带来的问题

  1. 事务一致性问题

    涉及分布式,就一定会涉及分布式一致性问题,

  2. 跨节点关联查询 join 问题

    切分之前,系统中很多列表和详情页所需的数据可以通过sql join来完成。而切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用join查询。

  3. 跨节点分页、排序、函数问题

    跨节点多库进行查询时,会出现limit分页、order by排序等问题。分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回

  4. 全局主键避重问题

    在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键重复问题。主流的唯一ID方案:

    • 利用数据库集群并设置相应的步长(Flickr方案)
    • Twitter Snowflake
    • 一大波GUID、Random算法
    • 时间戳标识码随机数组合策略,如美团使用的是:时间戳+用户标识码+随机数
  5. 数据迁移、扩容问题

    当业务高速发展,面临性能和存储的瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据迁移的问题。一般做法是先读出历史数据,然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS,以及业务发展的速度,进行容量规划,推算出大概需要多少分片(一般建议单个分片上的单表数据量不超过1000W)

    如果采用数值范围分片,只需要添加节点就可以进行扩容了,不需要对分片数据迁移。如果采用的是数值取模分片,则考虑后期的扩容问题就相对比较麻烦。

支持分库分表中间件

最后列举一些支持分库分表中间件

  • TSharding(蘑菇街)
  • sharding-jdbc(当当)
  • Atlas(奇虎360)
  • Cobar(阿里巴巴)
  • MyCAT(基于Cobar)
  • Oceanus(58同城)
  • Vitess(谷歌)

参考资料