分库分表原则剖析以及产生的问题如何解决

一、分库分表原则

1)、能不分就不分

MySQL是关系型数据库,数据库表之间的关系从一定的角度上映射了业务逻辑。任何分库分表的行为都会在某种程度上提升业务逻辑的复杂度,数据库除了承载数据的存储和访问外,协助业务更好的实现需求和逻辑也是其重要工作之一。

分库分表会带来数据的合并,查询或者更新条件的分离,事务的分离等等多种后果,业务实现的复杂程度往往会翻倍或者指数级上升。

所以,在分库分表之前,不要为了分而分,去做其他力所能及的事情,比如升级硬件,升级网络,升级数据库库版本,读写分离,负载均衡等。所有分库分表的前提是,这些都已经尽力了。

2)、数据量太大,正常的运维不满足正常业务访问

如果一张表数据量太大,会造成什么问题呢?我们以下面两个例子来看

对数据库的备份

        如果单表或者单个实例太大,在做备份的时候需要大量的磁盘IO或者网络IO资源。例如1T的数据,网络传输占用50MB的时候,需要20000秒才能传输完毕,在此整个过程中的维护风险都
是高于平时的。

对数据表的修改

        对数据表的修改。如果某个表过大,对此表做DDL的时候,MySQL会锁住全表,这个时间可能很长,在这段时间业务不能访问此表,影响甚大。

基于这些影响,如果一张表数据量大到影响我们正常业务了,就会做分库分表,这就是原则之一

3)、表设计不合理,需要对某些字段垂直拆分

举一个例子来讲解垂直拆分原则,如果一个用户表,在最初设计表的时候是这样的:

users表

字段名称字段类型备注
idbigint用户的ID
namevarchar用户名字
last_login_timedatetime最近登录时间
personal_infotext私人信息
xxxxxxxx其他信息字段

情况(1):

        你的业务中如果用户数从100W增长到10亿。你为了统计活跃用户,在每个人登录的时候都会记录一下他的最近登录时间(last_login_time)。并且用户活跃的很,不断的去update这个last_login_time字段值,那么这个表就会压力很大。

        站在业务的角度上,最好的办法就是先把last_login_time字段拆分出去,我们暂且叫他user_time。这样做的好处是业务的代码只有在用到这个字段的时候修改一下就行了。如果不这么做,直接把users表水平切分了,那么所有访问users表的地方都要进行修改。

情况(2):

        personal_info这个字段本来没啥用,就是让用户注册的时候填一些个人爱好而已,基本不查询。一开始的时候有没有无所谓,但是后来发现两个问题:

        一,这个字段占用了大量的空间,因为是text类型,有很多人喜欢长篇大论地介绍自己。更糟糕的是

        二,不知道哪天哪个产品经理心血来潮,说允许个人信息公开吧,以方便让大家更好的相互了解。那么在所有人猎奇窥私心理的影响下,对此字段的访问大幅度增加。数据库压力瞬间扛不住了,这个时候,只好考虑对这个表进行垂直拆分。

4)、某些数据表出现了无穷增长

有时候,在项目初期阶段,一张表的数据量增幅是比较稳定的,但是突然某天项目爆火,数据量激增,这种增长速率是不可控的,这个时候,增加存储,提升机器配置已经苍白无力了,水平切分原则是最佳实践。拆分的标准很多,按用户的,按时间的,按用途的等等方式进行拆分。

5)、安全性和可用性的考虑

安全性和可用性也是非常重要的原则,这个其实很容易理解,举个例子,比如鸡蛋不要放在一个篮子里,我不希望我的数据库出问题,但我希望在出问题的时候不要影响到100%的用户,这个影响的比例越少越好,那么,水平切分可以解决这个问题,把用户,库存,订单等等本来同统一的资源切分掉,每个小的数据库实例承担一小部分业务,这样整体的可用性就会提升。这对Qunar(去哪儿)这样的业务还是比较合适的,人与人之间,某些库存与库存之间,关联不太大,可以做一些这样的切分。所以基于此,就可以根据业务进行分库。根据业务,将不同业务的数据库解耦开来

6)、业务耦合性考虑

这个跟上面有点类似,主要是站在业务的层面上,我们的火车票业务和烤羊腿业务是完全无关的业务,虽然每个业务的数据量可能不太大,放在一个MySQL实例中完全没问题,但是很可能烤羊腿业务的DBA或者开发人员水平很差,动不动给你出一些幺蛾子,直接把数据库搞挂。这个时候,火车票业务的人员虽然技术很优秀,工作也很努力,照样被老板打屁股。解决的办法很简单:惹不起,躲得起。直接将火车业务和羊腿业务数据库拆分开

二、分库分表架构方案

接下来我们详细讲一下垂直拆分水平拆分

1、垂直拆分

垂直拆分常见有垂直分库和垂直分表两种。

1)、垂直分库

是根据数据库里面的数据库表的相关性进行拆分,比如:一个数据库里面既存在用户数据,又存在订单数据,那么垂直拆分可以吧用户数据放在用户库,把订单数据放到订单库。

另外在“微服务”盛行的今天已经非常普及,按照业务模块来划分不同的数据库,也是一种垂直拆分,而不是像早期一样将所有的数据表都放在同一个数据库中。如下图:

每个库的结构不一样

每个库的数据也不一样,没有交集

使用垂直分库的场景:

当系统绝对并发量上来了,并且可以抽象出单独的业务模块的情况下使用垂直分库方案

2)、垂直分表

在日常开发和设计中比较常见,通俗的说法叫做“大表拆小表”。

拆分是基于关系型数据库中的“列”(字段)进行的。

通常情况,某个表中的字段比较多,可以新建立一张"扩展表",将不经常使用或者长度较大的字段拆分出去放到“扩展表”中,如下图所示:

以字段为依据,按照字段的活跃性,将表中字段拆到不同的表中(主表和扩展表)

拆分字段的操作建议在数据库设计阶段就做好。如果是在发展过程中拆分,则需要改写以
前的查询语句,会额外带来一定的成本和风险,建议谨慎。

拆分之后:

  • 每个表的结构不一样
  • 每个表的数据也不一样,一般来说,每个表的字段只有有一列交集,一般是主键,用于关联数据
  • 所有表的并集是全部数据

使用垂直分表的场景:

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

3)、垂直拆分的优缺点

优点:

  • 拆分后业务清晰,拆分规则明确
  • 系统之间进行整合或扩展很容易
  • 按照成本,应用的等级,应用的类型等将表放在不同的机器上,便于管理
  • 便于实现动静分离,冷热分离的数据库表的设计模式
  • 数据维护简单。

缺点:

  • 主键出现冗余,需要管理冗余列。
  • 会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力。
  • 依然存在单表数据量过大的问题(需要水平拆分)。
  • 事务处理复杂。

2、水平拆分

水平拆分是通过某种策略将数据分片来存储,分为水平分表和水平分库两部分,每片数据会分散到不同的MySQL表或者库中,达到分布式的效果,能够支持非常大的数据量。

1)、水平分库

以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。

  • 每个库的结构都一样
  • 但每个库的数据不一样,没有交集
  • 所有库的数据并集是全部数据

使用水平分库的场景:

当系统绝对并发量上来了,分表难以从根本上解决问题,并且还没有明显的业务归属进行抽取业务的情况下使用水平分库方案。

2)、水平分表

以字段为依据,按照一定策略(hash,range等),将一个表中的数据拆分到多个表中。与分库很相似

使用水平分表的场景:

当系统绝对并发量没有上来,只是单表的数据量太多,影响SQL效率,加重了CPU负担,以致于成为瓶颈的情况下使用水平分表方案。

3)、水平拆分的优缺点

优点:

  • 单库单表的数据保持在一定的量级,有助于性能的提高
  • 切分的表结构相同,应用层改造较少,只需要增加路由规则即可
  • 提高了系统的稳定性和负载能力

缺点:

  • 切分后,数据是分散的,很难利用数据库的Join操作,跨库Join性能较差
  • 拆分规则难以抽象
  • 分片事务的一致性难以解决
  • 数据多次拓展难度跟维护量极

3、两种策略小结

垂直分库和水平分库的区别

  • 垂直分库是按照业务模块的不同进行拆分
  • 水平分库是按照一定的策略进行拆分,达到把一个库中的数据进行拆分到不同的数据库中。

垂直分表和水平分表的区别

  • 垂直分表是按照表中的冷热数据(活跃数据)进行拆分的,并且拆分完了之后,一般是分主表和拓展表,那么两张表的结构不一样,两张表的关联靠id来支持。
  • 水平分表是按照一定的策略进行拆分,目的是把单表的庞大的数量按照一定的策略分摊到拆分之后的小表中,拆分之后,每张表的结构是一样的,但是数据不一样。

三、分库分表产生的问题剖析

1、事务一致性问题

1)跨库事务

分库之后,当一个事务需要操作多个数据库实例时(例如更新用户表和订单表,但它们分布在不同的数据库中),传统单机数据库的事务特性(ACID)无法直接保证。这时就会遇到以下问题:

  • 原子性问题:如果事务中的某个步骤失败,回滚整个事务变得困难。
  • 一致性问题:部分操作成功,部分失败,可能导致数据不一致。
  • 隔离性问题:并发事务在不同库中可能互相影响,难以保证隔离性。
  • 持久性问题:部分数据库提交了事务,而另一部分却因为故障没有提交。

2)跨表事务

即使在同一个数据库实例中,如果数据被分散到不同的表中,也可能面临事务一致性问题。比如更新一个主表和多个分表时,很难在不引入额外复杂度的情况下确保事务的原子性。

解决方案可以参考下面这篇文章

分布式事务管理-Seata从入门到精通_seata入门-CSDN博客

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

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

以下是解决方案

1)全局表

全局表 是在每个数据库节点中都保存一份相同的数据表,这样在进行跨库查询时,可以直接在本地节点进行关联查询,避免跨节点数据传输。

由于在每个节点都存了相同数据,这很有可能带来数据不一致性问题,所以适用范围有限,主要集中在以下场景

适用于那些数据量较小、更新频率低、但查询频繁的基础数据或字典表。例如:系统配置、地区、商品分类、用户角色等。

实现方式

  1. 复制全局表:将全局表的数据复制到每个分库。
  2. 数据同步:通过定时任务或数据库同步工具,保持各节点的全局表数据一致。

2)字段冗余

字段冗余 是在数据表中增加冗余字段,将关联表的部分字段直接存储在主表中,避免关联查询。例如,在订单表中存储用户的基本信息。

适用于需要频繁关联查询但数据变化不频繁的场景,比如用户的姓名、商品名称等。

实现方式

  1. 在主表中添加关联表的字段作为冗余字段。
  2. 在插入或更新数据时,同时更新冗余字段。

3)数据组装

数据组装 是指在应用层分步查询各个数据库节点的数据,然后在应用代码中将数据进行关联和组装,而不是依赖数据库的 join 操作。比如在service中分步骤查询,先查到id列表,再拿id列表去查询需要关联的数据

适用于复杂的关联查询且分库分表无法避免的场景。

实现方式

  1. 分步查询:分别查询每个数据库节点上的数据。
  2. 应用层关联:在应用层将查询结果通过代码进行关联和合并。

4)ER分片

ER分片 是根据实体和关系的关联性进行分库分表,将关联性强的实体数据放在同一个数据库节点上,尽量避免跨库 join。

举个例子:

关系型数据库中,如果已经确定了表之间的关联关系(如订单表和订单详情表),并且将那些存在关联关系的表记录存放在同一个分片上,那么就能较好地避免跨分片 Join 的问题。

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

跨节点多库进行查询时,会出现 limit 分页、order by 排序等问题。

分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂

核心解决的思路就是:

        需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序。

最终返回给用户如下图:

上图只是取第一页的数据,对性能影响还不是很大。但是如果取得页数很大,情况就变得复杂的多。

因为各分片节点中的数据可能是随机的,为了排序的准确性,需要将所有节点的前N页数据都排序好做合并,最后再进行整体排序,这样的操作很耗费 CPU 和内存资源,所以页数越大,系统性能就会越差。

在使用 Max、Min、Sum、Count 之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总再次计算。

4、全局主键避重问题

在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成 ID 无法保证全局唯一。

更详细的可以参考下面这篇文章

分库分表背景下,千万级订单id的生成策略详解-CSDN博客

5、数据迁移、扩容问题

当业务高速发展、面临性能和存储瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据的迁移问题。

当业务高速发展、面临性能和存储瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据的迁移问题。

此外还需要根据当前的数据量的 QPS,以及业务发展速度,进行容量规划,推算出大概需要多少分片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值