由于业务需要,需要对Mysql数据库进行分库分表,故而最近一直在整理分库分表的相关知识,现手上的工作也告一段落了,抽空将自己最近的学习结果转化为博文,分享给大家,本博文打算做成一个系列的,首先是分库分表的理论知识的了解,其次是基于Java编程语言的分库分表的框架的开发,最后是分库分表的编制。让大家不仅仅从理论上了解mysql的分库分表,通过代码来更深层次的了解,理论是如何落地到实践的。最后非常感谢《可伸缩服务架构 框架与中间件》这本书的作者么,本博文的代码实现是参考此书,然后结合当前系统平台框架开发而成。
坚持我写作的一贯风格,我们先需要带着问题来了解mysql的分库分表
- 什么是分库分表,为什么我们需要分库分表
- 如何进行分库分表,有什么优缺点
- 对于分库分表有哪些架构设计,对于后期的扩容扩展怎么样
- 目前行业内流行的解决方案有哪些?各自有什么特点
- 自己设计一个数据库分库分表的框架,如何设计,需要考虑哪些因素
为什么需要分库分表
随着我们的系统运行,存储在关系型数据库的数据量会越来越大,系统的访问的压力也会随之增大,如果一个库中的表数据超过了一定的数量,比如说mysql中的表数据达到千万级别,就需要考虑进行分库分表;
其次随着表数据的不断增大,会发现,查询也随着变得缓慢,如果添加索引的话,会发现影响到了新增和删除的性能,如果我们将数据库分散到不同的表上,单表的索引大小就得到了控制,对索引以及表结构的变更会变得很方便和高效;
当数据库实例的吞吐量达到性能的瓶颈时,我们需要扩展数据库实例,让每个数据库实例承担其中一部分数据库的请求,分解总体的大请求量的压力;
在数据库进行扩容的时候对应用层的配置改变最少, 就需要在每个数据库实例中预留足够的数据库数量
以上的情况我们都可以使用分库分表,那么什么是分库分表呢?
简而言之就是数据拆分:将一个表结构分为多个表,或者将一个表数据分片后放入多个表,这些表可以放在同一个数据库里,也可以放到不同的数据库中,甚至可以放到不同的数据库实例中
数据拆分的方式
数据拆分有两种方式:
- 垂直拆分: 根据业务的维度,将原本一个库中的表拆分多个表,每个库中表与原有的结构不同
- 水平拆分: 根据分片算法,将一个库拆分成多个库,每个库依旧保留原有的结构
在实际的开发过程中,通常是先进行维度拆分形成微服务结构,然后再进行水平拆分
分库分表
比如我们有一张表,随着业务的不断进行,mysql中表中数据量达到了10亿,若是将数据存放在一张表中,则性能一定不会太好,根据我们使用的经验,mysql数据库一张表的数据记录极限一般在5000万左右,所以我们需要对进行分片存储(水平拆分),按照5000万一个单位来拆分的话,需要切片数量20个,也就是20个数据库表
如果将20个相同业务表存放在同一个数据库中,那么单个数据库实例的网卡I/O、内存、CPU和磁盘性能是有限的,随着数据库访问频率的增加,会导致单个数据库实例和数据库达到性能瓶颈,因此我们需要将20个表分到多个数据库和多个数据库实例中,具体的评估如下:
【TODO 对数据库实例和数据库表的数量的评估】
如何进行分库分表
分库分表是对数据库拆分的一种解决方案,根据实施切片逻辑的层次不同,我们将分库分表方案大致分为三大类:客户端分片、代理分片和支持事务的分布式数据库
- 客户端分片
所谓的客户端分片即在使用数据库的应用层直接操作分片逻辑,分片规则需要在同一个应用的多个节点间进行同步,每个应用层嵌入一个操作切片的逻辑实现。
在客户端分片,目前主要有以下三种方式:
- 在应用层直接实现
这是一种非常通用的解决方案,直接在应用层读取分片规则,解析分片规则,根据分片规则实现切分的路由逻辑,从应用层直接决定每次操作应该使用哪个数据库实例中的对应的数据库
这种解决方案虽然有一定的代码侵入,但是实现起来比较简单,但是切片的逻辑是自己开发的, 如果生产上遇到了问题,能快速定位解决;
当然这种方式也存在缺点:代码的耦合度比较高,其次这种实现方式会让数据库保持的链接比较多,这要看应用服务的节点数量,需要提前进行容量上的评估
- 通过定制JDBC协议实现
这种解决方案主要是为了解决1中解决方案中的代码耦合,通过定制JDBC协议来实现(主要是针对业务逻辑层提供与JDBC一致的接口),让分库分表在JDBC的内部实现
目前当当网开源的框架:Sharding JDBC 就是使用这种解决方案来实现的
- 通过定制ORM框架实现
目前ORM框架非常流行,流行的JPA、Mybatis和Hibernate都是优秀的ORM框架,通过定制ORM框架来实现分库分表方案,常见的有基于Mybatis的分库分表方案的解决;
<select id="selectUser" parameterType="java.util.Map" resultType="User">
select user_id as userId,user_name as userName
from user_#{index}
where user_id = #{userId}
</select>
- 代理分片
代理分片就是在应用层和数据库层之间添加一个代理层,把分片的路由规则配置在代理层,代理层对外提供与JDBC兼容的接口给应用层,在业务实现之后,在代理层配置路由规则即可;
这种方案的优点:让应用层的开发人员专注于业务逻辑的实现,把分库分表的配置留给代理层处理
同样的业务存在缺点:增加了代理层,这样的话对每个数据库操作都增加了一层网络传输,这对性能是有影响的,同时需要维护增加的代理层,也有了硬件成本,线上生产环境出现了问题,不能迅速定位,需要有一定的技术专家来维护
我们常见的 Mycat就是基于此种解决方案来实现的
- 支持事务的分布式数据库
支持分布式事务的框架,目前有OceanBase、TiDB框架,这些框架将可伸缩特定和分布式事务的实现包装到了分布式数据库内部实现,对使用者透明,使用者不需要直接控制这些特性,但是对事务的支持不如关系型数据,适合大数据日志系统、统计系统、查询系统、社交网站等
分库分表的架构设计
上面我们介绍过数据拆分的两种方式:垂直拆分和水平拆分;
拆分方式 | 优点 | 缺点 |
---|---|---|
垂直拆分 | 1. 拆分后业务清晰,拆分规则明确 2. 系统之间进行整合或扩展容易 3. 按照成本、应用等级、应用的类型等将表放到不同的机器上,便于管理 4.便于实现动静分离、冷热分离的数据库表的设计模式 5. 数据维护简单 | 1. 部分业务表无法进行关联、只能通过接口的方式来解决,提高了系统的复杂度 2. 受每种业务不同的限制,存在单库性能瓶颈,对数据扩展和性能提升不友好 3. 事务处理复杂 |
水平拆分 | 1. 单裤单表的数据保持一定的量级,有助于性能的提高 2. 切分的表的结构相同,应用层改造较少,只需要增加路由规则即可 3. 提高了系统的稳定性和负载能力 | 1. 切分后数据是分散的,很难利用数据库的关联查询,跨库查询性能较差 2. 拆分规则难以抽象 3. 分片数据的一致性难以解决 4. 数据扩容的难度和维护量极大 |
综上所述,我们发现垂直拆分和水平拆分具有共同点:
- 存在分布式事务问题
- 存在跨节点join的问题
- 存在跨节点合并排序、分页的问题
- 存在多数据源管理的问题
垂直拆分更偏向于业务拆分的过程,在技术上我们更倾向于水平切分的方案;
常见的分片策略:
- 按照哈希切片
对数据库的某个字段进行来求哈希,再除以分片总数后取模,取模后相同的数据为一个分片,这样将数据分成多个分片的方法叫做哈希分片
我们大多数在数据没有时效性的情况下使用哈希分片,就是数据不管是什么时候产生的,系统都需要处理或者查询;
优点 | 缺点 |
---|---|
数据切片比较均匀,数据压力分散的效果好 | 数据分散后,对于查询需求需要进行聚合处理 |
- 按照时间切片
按照时间的范围将数据分布到不同的分片上,比如我们可以将交易数据按照与进行切片,或者按照季度进行切片,由交易数据的多少来决定按照什么样的时间周期来进行切片
这种切片方式适合明显时间特点的数据,常见的就是订单历史查询
分布式事务
本博文不进行分布式事务的分析和实践,后期我会更新一系列的分布式事务的博文,一起探讨分布式事务的原理、解决方案和代码实践等,本博文简单介绍了分布式事务的解决方案;
上面说到的,不管是垂直拆分还是水平拆分,都有一个共同的问题:分布式事务
我们将单表的数据切片后存储在多个数据库甚至是多个数据库实例中,所以依靠数据库本身的事务机制不能满足需要,这时就需要用到分布式事务来解决了
三种解决方案
- 两阶段提交协议
两阶段提交协议中的两阶段是:准备阶段和提交阶段,两个阶段都是由事务管理器(协调者)发起,事务管理器能最大限度的保证跨数据库操作的事务的原子性。
具体的交互逻辑如下:
优点 | 缺点 |
---|---|
是分布式系统环境下最严格的事务实现防范, 保证了数据一致性和操作原子性 | 1. 难以进行水平伸缩,因为在提交事务过程中,事务管理器需要和每个参与者进行准备和提交的操作协调 2.每个参与者之间的协调需要时间,参与者一多的话,则锁定资源和消费资源之间的时间差就边长 3. 两阶段提交协议是阻塞协议,在极端情况下不能快速响应的话,会造成阻塞问题 |
- 最大努力保证模式
这是一种非常通用的保证分布式一致性的模式,适合对一致性要求不是十分严格的但是对性能要求比较高的场景
最大努力保证模式:在更新多个资源时,将多个资源的提交尽量延后到最后一刻进行处理,这样的话,如果业务流程出现问题,则所有的资源更新都可以回滚,事务仍然保持一致。
最大努力保证模式在发生系统问题,比如网络问题等会出现问题,造成数据一致性的问题 ,这是就需要进行实时补偿,将已提交的事务进行回滚
一般情况下,使用消息中间件来完成消费者之间的事务协调,客户端从消息中间件的队列中消费消息,更新数据库,此时会涉及到两个操作,一是从消息中间件消费消息,二是更新数据库,具体的操作步骤如下:
- 开启消息事务
- 接收消息
- 开启数据库事务
- 更新数据库
- 提交数据库事务
- 提交消息事务
上述步骤最关键的地方在5和6,如果5成功了,但是6出现了问题,导致消息中间件认为消息没有被成功消费,既有的机制会重新再消费消息,就会出现消息重复消费,这是需要幂等处理来避免消息的重新消费
其次我们还需要注意消息消费的顺序性问题,以及消费过程中是否调用远程接口等耗时操作
优点 | 缺点 |
---|---|
性能较高 | 1. 数据一致性不能完美保证,只能是最大保证 2. 可能出现消息重复消费(幂等处理) 3. 数据库事务可能存在远程操作嵌套,互相影响 |
- 事务补偿机制
以上提到的两种解决方案:两阶段提交协议对系统的性能影响较大,最大努力保证模式会是多个分布式操作互相嵌套,有可能互相影响,那么我们采用事务补偿机制:
事务补偿即在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须事务链中的每一个业务服务或操作都有对应的可逆服务。对于Service服务本身无状态,也不容易实现前面讨论过的通过DTC或XA机制实现的跨应用和资源的事务管理,建立跨资源的事务上下文.
我们通过跨银行转账来说明:
首先调用取款服务,完全调用成功并返回,数据已经持久化。然后调用异地的存款服务,如果也调用成功,则本身无任何问题。如果调用失败,则需要调用本地注册的逆向服务(本地存款服务),如果本地存款服务调用失败,则必须考虑重试,如果约定重试次数仍然不成功,则必须log到完整的不一致信息。也可以是将本地存款服务作为消息发送到消息中间件,由消息中间件接管后续操作。
最后添加的重试机制是最大程度的确保补偿服务执行,保持数据的一致性,如果重试之后还是失败,则将操作保存在消息中间件中,等待后续处理,这样就更多了一重保障
分库分表引起的问题
由于将完整的数据分成若干份,在以下的场景中会产生多种问题
- 扩容与迁移
在分库分表中,如果涉及的分片已经达到了承载数据的最大值,就需要对集群进行扩容,通常包括以下的步骤
- 按照新旧分片规则,对新旧数据库进行双写
- 将双写前按照旧分片规则写入的历史数据,根据新分片规则迁移写入新的数据库
- 将按照旧的分片规则查询改为按照新的分片规则查询
- 将双写数据库逻辑从代码中下线,只按照新的分片规则写入数据
- 删除按照旧分片规则写入的历史数据
2步骤中迁移数据时,数据量非常大,通常会导致不一致,因此需要先迁移旧的数据,洗完后再迁移到新规则的新数据库下,再做全量对比,对比评估在迁移过程中是否有数据的更新,如果有的话就再清洗、迁移,最后以对比没有差距为准
- 分库分表维度导致的查询问题
进行了分库分表以后,如果查询的标准是分片的主键,则可以通过分片规则再次路由并查询,但是对于其他主键的查询、范围查询、关联查询、查询结果排序等,并不是按照分库分表维度查询的;
这样的话,解决方案有以下三种:
- 在多个分片表中查询后合并数据集,这种方式的效率最低
- 冗余记录多份数据,方便查询, 缺点是需要额外维护一份数据,浪费资源
- 通过搜索引擎解决,但如果实时性要求很高,就需要实现实时搜索,可以利用大数据相关特性来解决
- 跨库事务难以实现
同时操作多个库,则会出现数据不一致的情况,此时可以引用分布式事务来解决
- 同组数据跨库问题
要尽量把同一组数据放到同一数据库服务器上,不但在某些场景下可以利用本地事务的强一致性,还可以是这组数据自治
主流的解决方案
目前针对mysql的分库分表,行业内主流的解决方案有:ShardingJDBC、Mycat
Mycat代理分片框架
Mycat是一款面向企业级应用的开源数据库中间件产品,他目前支持数据库集群,分布式事务与ACID,被普遍视为基于Mysql技术的集群分布式数据库解决方案
Mycat支持多种分片规则:
- 枚举法
- 固定分片的hash算法
- 范围约定
- 求模法
- 日期列分区法
- 通配取模
- ASCII码求模通配
- 编程指定
- 截取数据哈希解析
- 一致性Hash
具体的Mycat使用方法,以后应该会有一博文来整理,敬请期待啊~~~