Database Sharding

大多数软件项目都从链接到一个简单的单实例数据库开始。类似于Postgres或MySQL。

任何出现显着增长的应用程序或网站最终都需要扩展以适应流量的增长。对于数据驱动的应用程序和网站,扩展必须确保其数据的安全性和完整性。互联网业务的一个特点就是用户量巨大,BAT等头部公司都是亿级用户,产生的数据规模也飞速增长,传统的单库单表架构不足以支撑业务发展,存在下面的性能瓶颈:

分库分表背景

读写的数据量限制

数据库的数据量增大会直接影响读写的性能,比如一次查询操作,扫描 5 万条数据和 500 万条数据,查询速度肯定是不同的。数据库中的数据占用的空间越来越大,备份时间越来越长。

关于 MySQL 单库和单表的数据量限制,和不同的服务器配置,以及不同结构的数据存储有关,并没有一个确切的数字。这里参考阿里巴巴的《Java 开发手册》中数据库部分的建表规约:

单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

在新业务建表规划时,或者当前数据库单表已经超过对应的限制,可以进行分库分表,同时也要避免过度设计。因为分库分表虽然可以提高性能,但是盲目地进行分库分表只会增加系统的复杂度。

数据库连接限制

数据库的连接是有限制的,不能无限制创建,比如 MySQL 中可以使用 max_connections 查看默认的最大连接数,当访问连接数过多时,就会导致连接失败。以电商为例,假设存储没有进行分库,用户、商品、订单和交易,所有的业务请求都访问同一个数据库,产生的连接数是非常可观的,可能导致数据库无法支持业务请求。Mysql默认的最大连接数为100。这个连接连接数可以修改,而mysql服务允许的最大连接数为16384。

使用数据库连接池,可以优化连接数问题,但是更好的方式是通过分库等手段,避免数据库连接成为业务瓶颈。

除了这些,如果不进行数据库拆分,大量数据访问都集中在单台机器上,对磁盘 IO、CPU 负载等都会产生很大的压力,并且直接影响业务操作的性能。

什么是分库?

分库 就是将数据库中的数据分散到不同的数据库上,可以垂直分库,也可以水平分库。

垂直分库 就是把单一数据库按照业务进行划分,不同的业务使用不同的数据库,进而将一个数据库的压力分担到多个数据库。

举个例子:将数据库中的用户表、订单表和商品表分别单独拆分为用户数据库、订单数据库和商品数据库。

水平分库 是把同一个表按一定规则拆分到不同的数据库中,每个库可以位于不同的服务器上,这样就实现了水平扩展,解决了单表的存储和性能瓶颈的问题。

举个例子:订单表数据量太大,对订单表进行了水平切分(水平分表),然后将切分后的 2 张订单表分别放在两个不同的数据库。

什么是分表?

分表 就是对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分。

垂直分表 是对数据表列的拆分,把一张列比较多的表拆分为多张表。

举个例子:可以将用户信息表中的一些列单独抽出来作为一个表。

水平分表 是对数据表行的拆分,把一张行比较多的表拆分为多张表,可以解决单一表数据量过大的问题。

举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。

水平拆分只能解决单表数据量大的问题,为了提升性能,我们通常会选择将拆分后的多张表放在不同的数据库中。也就是说,水平分表通常和水平分库同时出现。

什么是分区

分表是将一张表分成N多个小表,分区是把一张表的数据分成N多个区块,这些区块可以在同一个磁盘上,也可以在不同的磁盘上。mysql的分表是真正的分表,一张表分成很多表后,每一个小表都是完整的一张表,都对应三个文件,一个.MYD数据文件,.MYI索引文件,.frm表结构文件。

常见的分片算法有哪些?

分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。

  • 哈希分片:求指定 key(比如 id) 的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,把数据和查询均匀地分布到所有分片中,不太适合经常需要范围查询的场景。
  • 范围分片:按照特性的范围区间(比如时间区间、ID 区间)来分配数据,比如 将 id1~299999 的记录分到第一个库, 300000~599999 的分到第二个库。范围分片适合需要经常进行范围查找的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。适合并发量不大的场景。
  • 地理位置分片:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。比如按照华东,华南,华北这样来区分业务,七牛云应该就是如此。
  • 融合算法:灵活组合多种分片算法,比如将哈希分片和范围分片人工组合。
  • 按照时间切分,就是将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。
  • …..

分表字段的选择

在分库分表的过程中,我们需要有一个字段用来进行分表,比如按照用户分表、按照时间分表、按照地区分表。这里面的用户、时间、地区就是所谓的分表字段。

那么,在选择这个分表字段的时候,一定要注意,要根据实际的业务情况来做慎重的选择。

比如说我们要对交易订单进行分表的时候,我们可以选择的信息有很多,比如买家Id、卖家Id、订单号、时间、地区等等,具体应该如何选择呢?

通常,如果有特殊的诉求,比如按照月度汇总、地区汇总等以外,我们通常建议大家按照买家Id进行分表。因为这样可以避免一个关键的问题那就是——数据倾斜(热点数据)。

能不能用 订单完成时间作为 Sharding Key 呢?比如说,分 12 个分片,每个月一个分片,这样对查询的兼容要好很多,毕竟查询条件中带上时间范围,让查询只落到某一个分片上,还是比较容易的,我在查询界面上强制用户必须指定时间范围就行了。

这种做法有个很大的问题,比如现在是 3 月份,那基本上所有的查询都集中在 3 月份这个分片上,其他 11 个分片都闲着,这样不仅浪费资源,很可能你 3 月那个分片根本抗不住几乎全部的并发请求。这个 问题就是「热点问题」

也就是说,我们 希望并发请求和数据能均匀地分布到每一个分片上,尽量避免出现热点。这是选择分片算法时需要考虑的一个重要的因素。一般常用的分片算法就那么几种,刚刚讲到的按照时间范围分片的方法是其中的一种。

基于范围来分片容易产生热点问题,不适合作为订单的分片方法,但是这种分片方法的 优点也很突出,那就是对查询非常友好,基本上只要加上一个时间范围的查询条件,原来该怎么查,分片之后还可以怎么查。范围分片特别适合那种数据量非常大,但并发访问量不大的 ToB 系统。比如说,电信运营商的监控系统,它可能要采集所有人手机的信号质量,然后做一些分析,这个数据量非常大,但是这个系统的使用者是运营商的工作人员,并发量很少。这种情况下就很适合范围分片。

一般来说,订单表都采用更均匀的哈希分片算法。比如说,我们要分 24 个分片,选定了 Sharding Key 是用户 ID,那我们决定某个用户的订单应该落到那个分片上的算法是,拿用户 ID 除以 24,得到的余数就是分片号。这是最简单的取模算法,一般就可以满足大部分要求了。当然也有一些更复杂的哈希算法,像一致性哈希之类的,特殊情况下也可以使用。

需要注意的一点是,哈希分片算法能够分得足够均匀的前提条件是,用户 ID 后几位数字必须是均匀分布的。比如说,你在生成用户 ID 的时候,自定义了一个用户 ID 的规则,最后一位 0 是男性,1 是女性,这样的用户 ID 哈希出来可能就没那么均匀,可能会出现热点。

还有一种分片的方法:查表法。查表法其实就是没有分片算法,决定某个 Sharding Key 落在哪个分片上,全靠人为来分配,分配的结果记录在一张表里面。每次执行查询的时候,先去表里查一下要找的数据在哪个分片中。

查表法的好处就是灵活,怎么分都可以,用上面两种分片算法都没法分均匀的情况下,就可以用查表法,人为地来把数据分均匀了。查表法还有一个特好的地方是,它的分片是可以随时改变的。比如我发现某个分片已经是热点了,那我可以把这个分片再拆成几个分片,或者把这个分片的数据移到其他分片中去,然后修改一下分片映射表,就可以在线完成数据拆分了。

分库分表后面临的问题

事务支持

分库分表后,同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。

跨库 join

分库分表后表之间的关联操作将受到限制,同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。无法join位于不同分库的表,也无法join分表粒度不同的表, 结果原本一次查询能够完成的业务,可能需要多次查询才能完成。

粗略的解决方法:

全局表:基础数据,所有库都拷贝一份。

字段冗余:这样有些字段就不用join去查询了。

系统层组装:分别查询出所有,然后组装起来,较复杂。

分布式 id

分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 id 了。

分库分表后,数据怎么迁移呢?

比较简单同时也是非常常用的方案就是停机迁移,写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。

如果你不想停机迁移数据的话,也可以考虑双写方案。双写方案是针对那种不能停机迁移的场景,实现起来要稍微麻烦一些。具体原理是这样的:

  • 对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。
  • 在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
  • 重复上一步的操作,直到老库和新库的数据一致为止。

想要在项目中实施双写还是比较麻烦的,很容易会出现问题。可以借助上面提到的数据库同步工具 Canal 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。

分库分表方案产品

ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar)是当当捐入 Apache 的,目前主要由京东维护。ShardingJdbc是一个本地数据库中间件框架,定位为轻量级Java框架,在Java的JDBC层提供的额外服务。使用客户端直连数据库,以jar包形式在本地应用层重写jdbc原生的方法,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。ShardingSphere 的功能完善,除了支持读写分离和分库分表,还提供分布式事务、数据库治理等功能。生态体系完善,社区活跃,文档完善,更新和发布比较频繁。

  • ShardingSphere优点
    • 程序自动完成,数据源方便管理
    • 支持sql标准下的任何数据库
  • ShardingSphere缺点
    • 以JAR的形式引入,存在代码入侵性,加大开发成本
    • 不能做到动态添加数据源,添加数据源还需要重启程序

基于代理方式的有MySQL Proxy, Amoeba, MyCat

MyCat 属于服务器分布式关系型数据库中间件,是一个基于第三方应用中间件数据库代理框架,客户端所有的jdbc请求都必须要先交给MyCat,再有MyCat转发具体的真实数据库服务器中。它支持分布式SQL查询,兼容MySQL通信协议,以Java生态支持多种后端数据库,通过数据分片提高数据查询处理能力。

  • MyCat优点:

    • 数据添加不会影响到程序

    • 应用层不需管理数据库层方面,由代理层去管理

    • 添加数据源不需要重启程序

  • MyCat缺点

    • 程序依赖的中间件,提高系统复杂性和维护工作

    • 中间件本身需要解决高可用问题

    • 增加了proxy,程序性能下降

基于Hibernate框架的是Hibernate Shards

基于mybatis的类似maven插件式的有蘑菇街的蘑菇街TSharding

通过重写spring的ibatis template类的Cobar Client。

TDDL 是淘宝开源的一个用于访问数据库的中间件, 它集成了分库分表, 读写分离,权重调配,动态数据源配置等功能。封装 jdbc 的 DataSource给用户提供统一的基于客户端的使用。

reference

What is data partitioning, and how to do it right

Understanding Database Sharding

Database Sharding vs. Partitioning

An Overview of Sharding Partitioning

分库分表实践

分库分表常见问题总结

MySQL:分库分表方案汇总