前言
从开发人员的角度来说,为什么要了解和掌握MySQL分片?
第一,了解MySQL分片可以更合理地定制分片策略,选分片字段是要讲科学的。
第二,了解MySQL分片以后如果出现故障报错,也有助于问题的排查。
第三,关系到开发过程中的代码调整,做分片后的MySQL数据库操作受到限制,比如join之类的操作,跨分片的操作,事务管理等,都是要注意的,可能需要代码的调整。
分区、分表、分片、分库的概念
那么首先,我们要理解一下分区、分表、分片、分库的概念。
分区,局限于单一数据库节点,将一张表分散存储在不同的物理块中。MySQL5以后支持分区,但是不支持二级分区,并且单机MySQL的性能远远不如Oracle,所以分区并不能解决性能问题。
分表,分表有两种,应用层面的分表和代理层面的分表。
应用层面的分表,人工把一张逻辑上完整的表分成若干个小表。比如T_PAYMENT表分为T_PAYMENT_0101…T_PAYMENT_1231。对代码不透明。
代理层面的分表,通过MERGE引擎或者代理中间件把表分成若干的字表,对应用只保留一个“表壳”,对代码透明。(MERGE引擎示意)
分表和分区比较类似,侧重点不同,分区侧重提高读写性能,分表侧重提高并发性能。两者不冲突,可以配合使用。
分库,单纯的分库就是垂直切分,把不同业务逻辑的表分开存储在在不同的数据库。
分片,分片就是分库+分表,属于水平切分,将表中数据按照某种规则放到多个库中,既分表又分库,就相当于原先一个库中的一个表,现在放到了好多个表里面,然后这好多个表又分散到了好多个库中。分片和分区也不冲突。
MySQL分片
分片键
数据分片是将一张分布式表按照指定的分片键(Partition Key)和分片模式(Partition Mode)水平拆分成多个数据片,分散在多个数据存储节点中。对于分片的表,要选取一个分片键。一张分布式表只能有一个分片键,分片键是用于划分和定位表的列,不能修改。
分片模式
•枚举/列表List
{1 => Cluster A, 2 => Cluster B}
•范围Range (仅支持数字或ASCII字符串类型的分片键)
{[1 - 100] => Cluster A, [101 - 199] => Cluster B}
•散列Hash (仅支持数字或ASCII字符串类型的分片键)
{1024n + 1 => Cluster A, 1024n + 2 => Cluster B}
分片方式类似于分区方式,可以选择枚举,范围Range或者散列哈希。不同的是分片不支持时间range。
分片策略
在做分片的时候,选择合适的分片规则非常重要,将极大地避免后续数据的处理难度,有以下几点需要关注:
1. 能不分就不分,对于1000万以内的表,不建议分片,通过合适的索引,读写分离等方式,可以更好地解决性能问题。
2. 分片数量不是越多越好,并且尽量均匀分布在多个存储节点上,只在必要的时候进行扩容,增加分片数量。
3. 分片键不能为空,不能修改,所以要选择表中中最常用且不变的字段。
4. 分片键选择时尽量做到可以将事务控制在分片范围内,可以避免出现跨分片的操作。
5. 选择分片规则时,要充分考虑数据的增长模式,数据的访问模式,分片关联 性问题,以及分片扩容问题。
总体上来说,分片的选择是取决于最频繁的查询 SQL 的条件。找出每个表最频繁的 SQL,分析其查询条件,以及相互的关系,并结合 ER 图,就能比较准确的选择每个表的分片策略。
MySQL分片要解决的问题
事务问题
分布式数据库分散在多个不同的物理主机,网络环境复杂,关注事务,保证数据一致性是一个问题。
先看单机环境下的事务管理,以Spring为例,利用注解的事务管理:
@Override
@Transactional
public void service(){
...
UserService.updateUserByUserId(userId);
...
OrderService.updateOrderByOrderNo(orderNo);
...
}
实际上这段代码相当于:
@Override
public void service(){
...
UserService.updateUserByUserId(userId);
...
OrderService.updateOrderByOrderNo(orderNo);
...
try{
commit();
}catch(Exception e){
rollback();
}
}
大概过程是这样:
变成分布式环境,变成了这样:
如何给多主机发commit并且保证他们最终状态一致就是分布式事务要解决的问题。
解决这个问题有两种方案。
第一种由应用程序和数据库共同控制,将一个跨多个数据库的分布式事务分拆成多个仅处于单个数据库上面的小事务,并通过应用程序来总控各个小事务。
@Override
public void service(){
...
userService();
...
try{
orderService();
}catch(Exception e){
rollback();
}
...
}
@Override
@Transactional
public void userService() throws RuntimeException{
...
UserService.updateUserByUserId(userId);
...
}
@Override
@Transactional
public void orderService() throws RuntimeException{
...
OrderService.updateOrderByOrderNo(orderNo);
...
}
}
这个方法是性能上比较高效,缺点就是需要应用程序在事务控制上做灵活设计。如果使用 了Spring的事务管理,改动起来会面临一定的困难。
第二种是交由数据库管理,简单有效。比较常见的方案有两阶段提交2PC、补偿事务TCC,MQ事务消息等。下图演示了2PC的过程。
除了图中提到的三个缺点,两阶段提交的方案影响的整体性能。
根据CAP原理,分布式数据库无法做到完全的ACID原则,几乎所有分布式解决方案都牺牲了部分一致性或者可用性。比如用最终一致性代替强一致性。
跨节点Join的问题
只要是进行切分,跨节点Join的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。
如果频次较高,可以考虑添加一张关联表,规避掉join操作,空间换时间。
在交易时直接将记录插入关联表,即可在查询时查询关联表获得想要的结果。
跨节点的count,order by,group by以及聚合函数问题
这些是一类问题,因为它们都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作。
解决方案:与解决跨节点join问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和join不同的是每个结点的查询可以并行执行,因此很多时候它的速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。
值得提醒的是,如果你的应用中包含大量的此类分析及聚合操作,考虑是否你对系统类型判断有误,是OLAP而非OLTP系统。对于OLAP系统不推荐使用MySQL更不推荐使用DRDS分布式数据库存储。
数据迁移,容量规划,扩容等问题
在前期如果对系统数据仓库的容量没有正确的预估,会遇到扩容问题。
利用对2的倍数取余具有向前兼容的特性(如对4取余得1的数对2取余也是1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。
举例说明,扩容前有两个集群作为存储节点,现要扩容为四个。
将原先的备机(A1,B1)升级为主机,然后再分别给他们配置备机,即完成了两倍的扩容。根据前面说的向前兼容的特性,分片规则不需要变化,是目前比较常用的扩容方案。
这部分实际和开发人员关系不大,了解即可。
ID问题
一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。
一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;
另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由。
比较常见的ID解决方案是应用生成UUID,或者在数据库中维护一个Sequence表。
跨分片的排序分页
一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:
上面图中所描述的只是最简单的一种情况(取第一页数据),看起来对性能的影响并不大。但是,如果想取出第10页数据,情况又将变得复杂很多,如下图所示:
有些读者可能并不太理解,为什么不能像获取第一页数据那样简单处理(排序取出前10条再合并、排序)。其实并不难理解,因为各分片节点中的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前N页数据都排序好后做合并,最后再进行整体的排序。很显然,这样的操作是比较消耗资源的,用户越往后翻页,系统性能将会越差。
那如何解决分库情况下的分页问题呢?有以下几种办法:
如果是在前台应用提供分页,则限定用户只能看前面n页,这个限制在业务上也是合理的,一般看后面的分页意义不大(如果一定要看,可以要求用户缩小范围重新查询)。
如果是后台批处理任务要求分批获取数据,则可以加大page size,比如每次获取5000条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。
分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。
分库策略
比较常见的两种分库策略,按照范围分库或者按照Mod分库,各有侧重点。
按照范围分库一般在前期数据量较小,在使用过程中用户数据不断增长,在数据库数量少时全库查询消耗小,后期调整数据库数量也比较容易。
按照Mod分库在初期就要规划好数据库数量,单库查询性能好,如果调整Mod因子会导致数据库迁移,后期增加数据库数量也比较麻烦。
实际使用中为了处理简单,一般选mod分库,数据库数量选择2的倍数,方便扩容。
分库数量
分库数量首先和单库能处理的记录数有关,一般来说,Mysql 单库超过5000万条记录,Oracle单库超过1亿条记录,DB压力就很大(当然处理能力和字段数量/访问模式/记录长度有进一步关系)。
在满足上述前提下,如果分库数量少,达不到分散存储和减轻DB性能压力的目的;如果分库的数量多,好处是每个库记录少,单库访问性能好,但对于跨多个库的访问,应用程序需要访问多个库,如果是并发模式,要消耗宝贵的线程资源;如果是串行模式,执行时间会急剧增加。
最后分库数量还直接影响硬件的投入,一般每个分库跑在单独物理机上,多一个库意味多一台设备。所以具体分多少个库,要综合评估,一般初次分库建议分4-8个库。
路由透明
分库从某种意义上来说,意味着DB schema改变了,必然影响应用,但这种改变和业务无关,所以要尽量保证分库对应用代码透明,分库逻辑尽量在数据访问层处理。
历史数据清理
历史数据的清理是一个比较棘手的问题。
假设我有一张订单表,联机系统只保存最近五天的订单,之前的要删除掉。
我们没办法根据日期进行分片,因为日期每天都在变化,按照日期分片意味着分片规则每天都在变化。这和常见的在Oracle定时任务删除分区和新建分区是不一样的。
假使我们根据设备进行分片,那么常规情况下,同一天的订单会均匀分布在各个分片上。最起码的原则是避免跨分片操作,因为跨分片操作性能极其低而且很多时候你也不知道会发生什么未知的事情。
所以比较容易想到的方法就是找到每个分片然后再每个分片上进行单独的数据清理操作。
如果是自研的分布式架构,应该可以通过脚本将清理语句分别路由到各个分片。如果是使用的开源中间件,大部分都会提供分片操作的方法(如果没有,那只能另寻他法了。)。如果是使用的商业服务的分布式数据库,也会提供分片数据清理操作的方法。
这部分我自己也确实没有其他更好的方案。
总结
解决了一个问题的同时却带来了这么多问题,MySQL的分片究竟是好是坏?我觉得全看各人取舍了。
下一篇将分享两个分布式数据库解决方案。
参考、引用如下:
《分库分表需要考虑的问题及方案》https://www.jianshu.com/p/32b3e91aa22c
《2PC两阶段提交协议》http://www.cnblogs.com/yuzhengzhong/p/9748082.html
《数据库Sharding集群:扩容问题解决方案》https://blog.csdn.net/wd2014610/article/details/81330655