分库分表

分库分表

一. 简述

1.1 为什么要分库分表?

思考几个问题:

(1)什么时候需要分库分表呢?如何判断需要分库分表?

(2)单表数据量达到什么级别需要考虑分库分表?

(3)数据单日增长速率达到什么程度需要考虑做分库分表?

为什么要分库分表?答案是数据库出现性能瓶颈。数据达到一定程度后,一些常见优化手段如优化索引,添加从库进行读写分离等也无法解决数据库的瓶颈问题,此时需要通过切分数据库到多个不同数据库来解决单一数据库的性能问题。

1.2 数据库瓶颈

数据库出现性能瓶颈的特征:

  • 大量请求阻塞:在高并发场景下,大量请求都需要操作数据库,导致连接数不够了,请求处于阻塞状态。

  • SQL 操作变慢:如果数据库中存在一张上亿数据量的表,一条 SQL 没有命中索引会全表扫描,这个查询耗时会非常久。

  • 存储出现问题:业务量剧增,单库数据量越来越大,给存储造成巨大压力。

从机器的角度看,性能瓶颈无非就是CPU、内存、磁盘、网络这些,要解决性能瓶颈最简单粗暴的办法就是提升机器性能,但是通过这种方法成本和收益投入比往往又太高了,不划算,所以重点还是要从软件角度入手。

数据库瓶颈:

  • I/O瓶颈

    • 磁盘读I/O瓶颈:热点数据太多,数据库缓存放不下,每次查询时会产生大量的I/O,降低查询速度 -> 分库和垂直分表
    • 网络IO瓶颈:请求的数据太多,网络带宽不够 -> 分库
  • CPU瓶颈

    • SQL问题:如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。
    • 单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表

1.3 什么是分库分表技术?

如字面意思,即拆分数据库和拆分单表:

  • 垂直切分:垂直切分就是模块划分,把同一个模块或者说联系紧密的表切分到一个server上,适合于因为表多而导致数据量大的场景;
  • 水平切分:而水平切分则是把表按照某种规则(比如对ID进行散列)切分到不同的server上,适合单表数据过多导致数据量大的场景。

垂直拆分比较简单,也就是本来一个数据库,数据量大之后,从业务角度进行拆分多个库。如下图,独立的拆分出订单库和用户库。

图片

水平拆分的概念,是同一个业务数据量大之后,进行水平拆分。图中订单数据达到了4000万,我们也知道mysql单表存储量推荐是百万级,如果不进行处理,mysql单表数据太大,会导致性能变慢。使用方案可以参考数据进行水平拆分。把4000万数据拆分4张表或者更多。当然也可以分库,再分表;把压力从数据库层级分开。

图片

实际场景中往往需要综合两种切分,将原数据库切分为可无限扩充的数据库阵列。

需要注意的是若综合使用时,切分策略可能会相互影响。如引入水平切分后,表间的关联关系就会受到制约,一般只允许主表(即散列的表)和其次表之间保持关联,所以就不能简单的以功能模块来划分,而是要更加细粒度的进行垂直切分,此概念类似于领域驱动设计中的”聚合”,每个划分(shard)的主表都是一个聚合中的聚合根。为了避免过度的切分,需要考虑将业务接近,数据增长速率接近的shard合并到一个数据源中,每个shard都还是独立的,它们有各自的主表,并使用各自主表ID进行散列,不同的是他们的散列取模即节点数量需要一致。

1.4 数据库常见优化方案

数据库优化方案很多,主要分为两大类:

  • 软件层面:SQL 调优、表结构优化、读写分离、数据库集群、分库分表等;
  • 硬件层面:主要是增加机器性能。

(1)SQL 调优

SQL 调优往往是解决数据库问题的第一步,往往投入少部分精力就能获得较大的收益。SQL 调优主要目的是尽可能的让那些慢 SQL 变快,手段其实也很简单就是让 SQL 执行尽量命中索引。

  • 开启慢 SQL 记录:如果你使用的是 Mysql,需要在 Mysql 配置文件中配置几个参数即可。

    1
    2
    3
    slow_query_log=on
    long_query_time=1
    slow_query_log_file=/path/to/log
  • 调优的工具:常常会用到 explain 这个命令来查看 SQL 语句的执行计划,通过观察执行结果很容易就知道该 SQL 语句是不是全表扫描、有没有命中索引。

    1
    select id, age, gender from  user where name = '爱笑的架构师';

    返回有一列叫“type”,常见取值有:ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)ALL 代表这条 SQL 语句全表扫描了,需要优化。一般来说需要达到range 级别及以上。

详细内容可以参考《高性能MySQL》(五)查询性能优化

(2)表结构优化

方案:数据库表冗余字段

以一个场景举例说明:User 表中有 {user_id、nickname} 等字段,Order 表中有 {order_id、user_id} 等字段,如果在查询订单时想拿到用户昵称怎么办?一般情况是通过 join 关联表操作,在查询订单表时关联查询用户表,从而获取导用户昵称。

但是随着业务量增加,订单表和用户表肯定也是暴增,这时候通过两个表关联数据比较耗时。可以尝试将 nickname 这个字段加到 Order 表中 {order_id、user_id、nickname} 。

冗余字段的做法也有一个弊端,如果这个字段更新会同时涉及到多个表的更新,因此在选择冗余字段时要尽量选择不经常更新的字段。

(3)架构优化

  • 当单台数据库实例扛不住,可以增加实例组成集群对外服务。
  • 当发现读请求明显多于写请求时,我们可以让主实例负责写,从实例对外提供读的能力;
  • 如果读实例压力依然很大,可以在数据库前面加入缓存如 Redis,让请求优先从缓存取数据减少数据库访问。

缓存分担了部分压力后,数据库依然是瓶颈,这个时候就可以考虑分库分表的方案。

(4)硬件优化

硬件成本非常高,一般来说不可能遇到数据库性能瓶颈就去升级硬件。在前期业务量比较小的时候,升级硬件数据库性能可以得到较大提升。但是在后期,升级硬件得到的收益就不那么明显了。

二. 分库分表实现方案

以一个商城系统为例逐步讲解数据库是如何一步步演进。

2.1 单应用单数据库

在早期阶段,一个商城系统就是一个系统包含多个基础功能模块,最后打包成一个 war 包部署,这就是典型的单体架构应用。

图片

如上图,商城系统包括主页 Portal 模板、用户模块、订单模块、库存模块等,所有的模块都共有一个数据库,通常数据库中有非常多的表。因为用户量不大,这样的架构在早期完全适用。

2.2 多应用单数据库

在前期为了抢占市场,这一套系统不停地迭代更新,代码量越来越大,架构也变得越来越臃肿,现在随着系统访问压力逐渐增加,系统拆分就势在必行了。为了保证业务平滑,系统架构重构也是分了几个阶段进行。

第一个阶段将商城系统单体架构按照功能模块拆分为子服务,比如:Portal 服务、用户服务、订单服务、库存服务等。

图片

如上图,多个服务共享一个数据库,这样做的目的是底层数据库访问逻辑可以不用动,将影响降到最低。

2.3 多应用多数据库

随着业务推广力度加大,数据库终于成为了瓶颈,这个时候多个服务共享一个数据库基本不可行了。我们需要将每个服务相关的表拆出来单独建立一个数据库,这其实就是“分库”了。

单数据库的能够支撑的并发量是有限的,拆成多个库可以使服务间不用竞争,提升服务的性能。

图片

如上图,从一个大的数据中分出多个小的数据库,每个服务都对应一个数据库,这就是系统发展到一定阶段必要要做的“分库”操作。如果只拆分应用不拆分数据库,不能解决根本问题,整个系统也很容易达到瓶颈。

2.4 分表

  • 什么时候分表?

    • 如果系统处于高速发展阶段,拿商城系统来说,一天下单量可能几十万,那数据库中的订单表增长就特别快,增长到一定阶段数据库查询效率就会出现明显下降。
    • 因此,当单表数据增量过快,比如超过500万的数据量就要考虑分表了。当然500万只是一个经验值,大家可以根据实际情况做出决策。
  • 那如何分表呢?分表有几个维度:

    • 一是水平切分和垂直切分,
    • 二是单库内分表和多库内分表。

(1)水平拆分和垂直拆分

就拿用户表 User 来说,表中有7个字段:{id,name,age,sex,nickname,description},如果 nickname 和 description 不常用,我们可以将其拆分为另外一张表:用户详细信息表,这样就由一张用户表拆分为了用户基本信息表+用户详细信息表,两张表结构不一样相互独立。

但是从这个角度来看垂直拆分并没有从根本上解决单表数据量过大的问题,因此我们还是需要做一次水平拆分。

图片拆分表

水平拆分的方法,比如表中有一万条数据,我们拆分为两张表:

  • id 为奇数的:1,3,5,7……放在 user1,
  • id 为偶数的:2,4,6,8……放在 user2 中。

水平拆分的方式也很多,除了上面说的按照 id 拆表,还可以按照时间维度取拆分,比如订单表,可以按每日、每月等进行拆分。

  • 每日表:只存储当天的数据。
  • 每月表:可以起一个定时任务将前一天的数据全部迁移到当月表。
  • 历史表:同样可以用定时任务把时间超过 30 天的数据迁移到 history表。

总结一下水平拆分和垂直拆分的特点:

  • 垂直切分:基于表或字段划分,表结构不同。
  • 水平切分:基于数据划分,表结构相同,数据不同。

(2)单库内拆分和多库拆分

拿水平拆分为例,每张表都拆分为了多个子表,多个子表存在于同一数据库中。比如下面用户表拆分为用户1表、用户2表。

图片单库拆分

在一个数据库中将一张表拆分为几个子表在一定程度上可以解决单表查询性能的问题,但是也会遇到一个问题:单数据库存储瓶颈。

所以在业界用的更多的还是将子表拆分到多个数据库中。比如下图中,用户表拆分为两个子表,两个子表分别存在于不同的数据库中。

图片多库拆分

分表主要是为了减少单张表的大小,解决单表数据量带来的性能问题。

2.5 分库分表步骤

  1. 根据容量(当前容量和增长量)评估分库或分表个数。
  2. 选key(均匀)。
  3. 分表规则(hash或range等)。
  4. 执行(一般双写)。
  5. 扩容问题(尽量减少数据的移动)。

2.6 总结

(1)水平分库

preview

  1. 概念:以字段为依据,按照一定策略(hash、range等),将一个中的数据拆分到多个中。
  2. 结果:
  • 每个结构都一样;
  • 每个数据都不一样,没有交集;
  • 所有并集是全量数据;

场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。

分析:库多了,io和cpu的压力自然可以成倍缓解。

(2)水平分表

img

  1. 概念:以字段为依据,按照一定策略(hash、range等),将一个中的数据拆分到多个中。
  2. 结果:
  • 每个结构都一样;
  • 每个数据都不一样,没有交集;
  • 所有并集是全量数据;

场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。

分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。

(3)垂直分库

preview

  1. 概念:以为依据,按照业务归属不同,将不同的拆分到不同的中。
  2. 结果:
  • 每个结构都不一样;
  • 每个数据也不一样,没有交集;
  • 所有并集是全量数据;

场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。

分析:到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。

(4)垂直分表

img

  1. 概念:以字段为依据,按照字段的活跃性,将中字段拆到不同的(主表和扩展表)中。
  2. 结果:
  • 每个结构都不一样;
  • 每个数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;
  • 所有并集是全量数据;

场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。

分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。

三. 分库分表要解决的问题

既然分库分表这么好,那我们是不是在项目初期就应该采用这种方案呢?分库分表的确解决了很多问题,但是也给系统带来了很多复杂性。

2.1 跨库关联查询(跨节点Join)

在单库未拆分表之前,我们可以很方便使用 join 操作关联多张表查询数据,但是经过分库分表后两张表可能都不在一个数据库中,如何使用 join 呢?

解决方案:

  • 字段冗余:把需要关联的字段放入主表中,避免 join 操作。
  • 数据抽象:通过ETL等将数据汇合聚集,生成新的表。
  • 全局表:比如一些基础表可以在每个数据库中都放一份。
  • 应用层组装:将基础数据查出来,通过应用程序计算组装;分两次查询实现。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。

复杂查询:垂直切分后,就跟join说拜拜了;水平切分后,查询的条件一定要在切分的维度内,比如查询具体某个用户下的各位订单等;禁止不带切分的维度的查询,即使中间件可以支持这种查询,可以在内存中组装,但是这种需求往往不应该在在线库查询,或者可以通过其他方法转换到切分的维度来实现。

2.2 分布式事务

单数据库可以用本地事务搞定,使用多数据库解决事务问题目前有两种可行的方案:

  • 分布式事务:
    • 优点:交由数据库管理,简单有效;基于两阶段提交,最大限度地保证了跨数据库操作的“原子性”,是分布式系统下最严格的事务实现方式。实现简单,工作量小。由于多数应用服务器以及一些独立的分布式事务协调器做了大量的封装工作,使得项目中引入分布式事务的难度和工作量基本上可以忽略不计。
    • 缺点:性能代价高,特别是shard越来越多时;系统“水平”伸缩的死敌。基于两阶段提交的分布式事务在提交事务时需要在多个节点之间进行协调,最大限度地推后了提交事务的时间点,客观上延长了事务的执行时间,这会导致事务在访问共享资源时发生冲突和死锁的概率增高,随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平伸缩的”枷锁”, 这是很多Sharding系统不采用分布式事务的主要原因。
    • 常用解决方案有:基于可靠消息(MQ)的解决方案、两阶段事务提交、柔性事务等。
  • 通过应用程序与数据库共同控制实现事务:
    • 优点:性能上有优势
    • 缺点:需要应用程序在事务控制上做灵活设计。如果使用 了spring的事务管理,改动起来会面临一定的困难。
    • 原理:将一个跨多个数据库的分布式事务分拆成多个仅处 于单个数据库上面的小事务,并通过应用程序来总控 各个小事务。

详细内容可以参考《分布式事务》。

2.3 分布式ID

Mysql 数据库在单库单表可以使用 id 自增作为主键,分库分表了之后就不行了,会出现 id 重复。

常用的分布式 ID 解决方案有:

  • UUID
  • 基于数据库自增单独维护一张 ID表
  • 号段模式
  • Redis 缓存
  • 雪花算法(Snowflake)
  • 百度uid-generator
  • 美团Leaf
  • 滴滴Tinyid

唯一ID方案

这个方案也很多,主流的有那么几种:

1. 利用数据库自增ID

优点:最简单。
缺点:单点风险、单机性能瓶颈。

2. 利用数据库集群并设置相应的步长(Flickr方案)

优点:高可用、ID较简洁。
缺点:需要单独的数据库集群。

3. Twitter Snowflake

优点:高性能高可用、易拓展。
缺点:需要独立的集群以及ZK。

4. 一大波GUID、Random算法

优点:简单。
缺点:生成ID较长,有重复几率。

我们的方案

为了减少运营成本并减少额外的风险我们排除了所有需要独立集群的方案,采用了带有业务属性的方案:

时间戳+用户标识码+随机数

有下面几个好处:

  • 方便、成本低。
  • 基本无重复的可能。
  • 自带分库规则,这里的用户标识码即为用户ID的后四位,在查询的场景下,只需要订单号就可以匹配到相应的库表而无需用户ID,只取四位是希望订单号尽可能的短一些,并且评估下来四位已经足够。
  • 可排序,因为时间戳在最前面。

当然也有一些缺点,比如长度稍长,性能要比int/bigint的稍差等。

详细内容可以参考《分布式ID》。

2.4 排序、分页、函数计算问题

跨节点的 Countorder bygroup by 以及聚合函数这一类问题,因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。

解决方案:与解决跨节点Join问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和join不同的是每个结点的查询可以并行执行,因此很多时候它的速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。

2.5 跨分片的排序分页

一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:

上面图中所描述的只是最简单的一种情况(取第一页数据),看起来对性能的影响并不大。但是,如果想取出第10页数据,情况又将变得复杂很多,如下图所示:

因为各分片节点中的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前N页数据都排序好后做合并,最后再进行整体的排序。很显然,这样的操作是比较消耗资源的,用户越往后翻页,系统性能将会越差。

那如何解决分库情况下的分页问题呢?

有以下几种办法:

  • 如果是在前台应用提供分页,则限定用户只能看前面n页,这个限制在业务上也是合理的,一般看后面的分页意义不大(如果一定要看,可以要求用户缩小范围重新查询)。
  • 如果是后台批处理任务要求分批获取数据,则可以加大page size,比如每次获取5000条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。
  • 分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。

2.6 数据迁移,容量规划,扩容等问题

来自淘宝综合业务平台团队,它利用对2的倍数取余具有向前兼容的特性(如对4取余得1的数对2取余也是1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了Sharding扩容的难度。

数据库拆分一般是业务发展到一定规模后的优化和重构,为了支持业务快速上线,很难一开始就分库分表,垂直拆分还好办,改改数据源就搞定了,一旦开始水平拆分,数据清洗就是个大问题,为此,我们经历了以下几个阶段。

第一阶段

img

  • 数据库双写(事务成功以老模型为准),查询走老模型。
  • 每日job数据对账(通过DW),并将差异补平。
  • 通过job导历史数据。

第二阶段

img

  • 历史数据导入完毕并且数据对账无误。
  • 依然是数据库双写,但是事务成功与否以新模型为准,在线查询切新模型。
  • 每日job数据对账,将差异补平。

第三阶段

img

  • 老模型不再同步写入,仅当订单有终态时才会异步补上。
  • 此阶段只有离线数据依然依赖老的模型,并且下游的依赖非常多,待DW改造完就可以完全废除老模型了。

2.7 水平切分策略

(1)查询切分

通过一个中间库记录ID和分库的映射关系

优点:ID和分库的映射算法可以任意修改

缺点:需要引入额外的单点

将ID和库的Mapping关系记录在一个单独的库中。

优点:ID和库的Mapping算法可以随意更改。
缺点:引入额外的单点。

img

(2)范围切分

通过时间区间或ID区间等来划分

优点:单表大小可控,天然水平扩展

缺点:无法解决集中写入瓶颈问题

比如按照时间区间或ID区间来切分。

优点:单表大小可控,天然水平扩展。
缺点:无法解决集中写入瓶颈的问题。

img

range方案也就是以范围进行拆分数据。

图片

range方案比较简单,就是把一定范围内的订单,存放到一个表中;如上图id=12放到0表中,id=1300万的放到1表中。设计这个方案时就是前期把表的范围设计好。通过id进行路由存放。

  • 优点

我们小伙伴们想一下,此方案是不是有利于将来的扩容,不需要做数据迁移。即时再增加4张表,之前的4张表的范围不需要改变,id=12的还是在0表,id=1300万的还是在1表,新增的4张表他们的范围肯定是 大于 4000万之后的范围划分的。

  • 缺点

有热点问题,我们想一下,因为id的值会一直递增变大,那这段时间的订单是不是会一直在某一张表中,如id=1000万 ~ id=2000万之间,这段时间产生的订单是不是都会集中到此张表中,这个就导致1表过热,压力过大,而其他的表没有什么压力。

(3)Hash切分

Hash切分:通过mod求余划分,推荐采用mod 2^n这种一致性Hash。

比如分库分表方案为32*32,通过对ID后四位对32求余划分到32个分库中,然后把ID后四位div 32 mod 32将各分库分为32张表,总共1024张表,线上部署为8个主从集群,每个集群4个库。

每次达到瓶颈后都可以通过修改求余策略来扩展数据集群,易于水平扩展。

分库维度确定后,如何把记录分到各个库里呢?

一般有两种方式:

  • 根据数值范围,比如用户Id为1-9999的记录分到第一个库,10000-20000的分到第二个库,以此类推。
  • 根据数值取模,比如用户Id mod n,余数为0的记录放到第一个库,余数为1的放到第二个库,以此类推。

优劣比较:

评价指标按照范围分库按照Mod分库

库数量前期数目比较小,可以随用户/业务按需增长前期即根据mode因子确定库数量,数目一般比较大

访问性能前期库数量小,全库查询消耗资源少,单库查询性能略差前期库数量大,全库查询消耗资源多,单库查询性能略好

调整库数量比较容易,一般只需为新用户增加库,老库拆分也只影响单个库困难,改变mod因子导致数据在所有库之间迁移

数据热点新旧用户购物频率有差异,有数据热点问题新旧用户均匀到分布到各个库,无热点

实践中,为了处理简单,选择mod分库的比较多。同时二次分库时,为了数据迁移方便,一般是按倍数增加,比如初始4个库,二次分裂为8个,再16个。这样对于某个库的数据,一半数据移到新库,剩余不动,对比每次只增加一个库,所有数据都要大规模变动。

补充下,mod分库一般每个库记录数比较均匀,但也有些数据库,存在超级Id,这些Id的记录远远超过其他Id,比如在广告场景下,某个大广告主的广告数可能占总体很大比例。如果按照广告主Id取模分库,某些库的记录数会特别多,对于这些超级Id,需要提供单独库来存储记录。

图片

在我们设计系统之前,可以先预估一下大概这几年的订单量,如:4000万。每张表我们可以容纳1000万,也我们可以设计4张表进行存储。

那具体如何路由存储的呢?hash的方案就是对指定的路由key(如:id)对分表总数进行取模,上图中,id=12的订单,对4进行取模,也就是会得到0,那此订单会放到0表中。id=13的订单,取模得到为1,就会放到1表中。为什么对4取模,是因为分表总数是4。

  • 优点

订单数据可以均匀的放到那4张表中,这样此订单进行操作时,就不会有热点问题。

热点的含义:热点的意思就是对订单进行操作集中到1个表中,其他表的操作很少。订单有个特点就是时间属性,一般用户操作订单数据,都会集中到这段时间产生的订单。如果这段时间产生的订单 都在同一张订单表中,那就会形成热点,那张表的压力会比较大。

  • 缺点

将来的数据迁移和扩容,会很难

如:业务发展很好,订单量很大,超出了4000万的量,那我们就需要增加分表数。如果我们增加4个表

图片

一旦我们增加了分表的总数,取模的基数就会变成8,以前id=12的订单按照此方案就会到4表中查询,但之前的此订单时在0表的,这样就导致了数据查不到。就是因为取模的基数产生了变化。

遇到这个情况,我们小伙伴想到的方案就是做数据迁移,把之前的4000万数据,重新做一个hash方案,放到新的规划分表中。也就是我们要做数据迁移。这个是很痛苦的事情。有些小公司可以接受晚上停机迁移,但大公司是不允许停机做数据迁移的。

当然做数据迁移可以结合自己的公司的业务,做一个工具进行,不过也带来了很多工作量,每次扩容都要做数据迁移

那有没有不需要做数据迁移的方案呢,我们看下面的方案

hash取模方案:没有热点问题,但扩容迁移数据痛苦
range方案:不需要迁移数据,但有热点问题。

一般采用Mod来切分,下面着重讲一下Mod的策略。

img

数据水平切分后我们希望是一劳永逸或者是易于水平扩展的,所以推荐采用mod 2^n这种一致性Hash。

以统一订单库为例,我们分库分表的方案是32*32的,即通过UserId后四位mod 32分到32个库中,同时再将UserId后四位Div 32 Mod 32将每个库分为32个表,共计分为1024张表。线上部署情况为8个集群(主从),每个集群4个库。

为什么说这种方式是易于水平扩展的呢?我们分析如下两个场景。

场景一:数据库性能达到瓶颈

方法一

按照现有规则不变,可以直接扩展到32个数据库集群。

img

方法二

如果32个集群也无法满足需求,那么将分库分表规则调整为(322^n)(32/2^n),可以达到最多1024个集群。

img

场景二:单表容量达到瓶颈(或者1024已经无法满足你)

方法:

img

假如单表都已突破200G,2001024=200T(按照现有的订单模型算了算,大概一万千亿订单,相信这一天,嗯,指日可待!),没关系,32(32*2^n),这时分库规则不变,单库里的表再进行裂变,当然,在目前订单这种规则下(用userId后四位 mod)还是有极限的,因为只有四位,所以最多拆8192个表,至于为什么只取后四位,后面会有篇幅讲到。

2.8 分库数量

分库数量首先和单库能处理的记录数有关,一般来说,Mysql 单库超过5000万条记录,Oracle单库超过1亿条记录,DB压力就很大(当然处理能力和字段数量/访问模式/记录长度有进一步关系)。

在满足上述前提下,如果分库数量少,达不到分散存储和减轻DB性能压力的目的;如果分库的数量多,好处是每个库记录少,单库访问性能好,但对于跨多个库的访问,应用程序需要访问多个库,如果是并发模式,要消耗宝贵的线程资源;如果是串行模式,执行时间会急剧增加。

最后分库数量还直接影响硬件的投入,一般每个分库跑在单独物理机上,多一个库意味多一台设备。所以具体分多少个库,要综合评估,一般初次分库建议分4-8个库。

2.9 路由透明

分库从某种意义上来说,意味着DB schema改变了,必然影响应用,但这种改变和业务无关,所以要尽量保证分库对应用代码透明,分库逻辑尽量在数据访问层处理。当然完全做到这一点很困难,具体哪些应该由DAL负责,哪些由应用负责,这里有一些建议:

对于单库访问,比如查询条件指定用户Id,则该SQL只需访问特定库。此时应该由DAL层自动路由到特定库,当库二次分裂时,也只要修改mod 因子,应用代码不受影响。

对于简单的多库查询,DAL负责汇总各个数据库返回的记录,此时仍对上层应用透明。

2.10 常用的分库分表中间件

目前市面上的分库分表中间件相对较多,其中基于代理方式的有MySQL Proxy和Amoeba,基于Hibernate框架的是Hibernate Shards,基于jdbc的有当当sharding-jdbc,基于mybatis的类似maven插件式的有蘑菇街的蘑菇街TSharding,通过重写spring的ibatis template类是Cobar Client,这些框架各有各的优势与短板,架构师可以在深入调研之后结合项目的实际情况进行选择,但是总的来说,我个人对于框架的选择是持谨慎态度的。一方面多数框架缺乏成功案例的验证,其成熟性与稳定性值得怀疑。另一方面,一些从成功商业产品开源出框架(如阿里和淘宝的一些开源项目)是否适合你的项目是需要架构师深入调研分析的。当然,最终的选择一定是基于项目特点、团队状况、技术门槛和学习成本等综合因素考量确定的。

四. 实战

4.1 实战场景-Mycat实现分库分表

暂无

分库分表示例GitHub地址:https://github.com/littlecharacter4s/study-sharding

来源:http://cnblogs.com/littlechara


参考:

🔗 大众点评订单系统分库分表实践

🔗 分库分表需要考虑的问题及方案

🔗 我们为什么要分库分表?

🔗 你知道怎么分库分表吗?如何做到永不迁移数据和避免热点吗?

🔗 MySQL:互联网公司常用分库分表方案汇总

🔗关于分布式事务、两阶段提交、一阶段提交、Best Efforts 1PC模式和事务补偿机制的研究