best365官网登录入口

都2025年了,还不懂分库分表吗,一文看懂分库分表

发布时间 2025-07-23 23:27:06 作者 admin 阅读 388

一.什么是分库分表

分表:将一个表中的数据按照某种规则分拆到多张表中,降低锁粒度以及索引树,提升数据查询效率。分库:将一个数据库中的数据按照某种规则分拆到多个数据库中,以缓解单服务器的压力(CPU、内存、磁盘、IO)。

二.为什么要分库分表

性能角度:CPU、内存、磁盘、IO瓶颈

随着业务体量扩大,数据规模达到百万行,数据库索引树庞大,查询性能出现瓶颈。用户并发流量规模扩大,由于单库(单服务器)物理性能限制也无法承载大流量。

可用性角度:单机故障率影响面

如果是单库,数据库宕机会导致100%服务不可用,N库则可以将影响面降低N倍。

三. 什么时候才考虑分库分表呢?

4.1 什么时候分表?

如果你的系统处于快速发展时期,如果每天的订单流水都新增几十万,并且,订单表的查询效率明变慢时,就需要规划分库分表了。一般B+树索引高度是2~3层最佳,如果数据量千万级别,可能高度就变4层了,数据量就会明显变慢了。不过业界流传,一般500万数据就要考虑分表了。

4.2 什么时候分库

业务发展很快,还是多个服务共享一个单体数据库,数据库成为了性能瓶颈,就需要考虑分库了。比如订单、用户等,都可以抽取出来,新搞个应用(其实就是微服务思想),并且拆分数据库(订单库、用户库)。

四.拆分类型

3.1 垂直拆分

3.1.1 垂直分库

垂直拆库则在垂直拆表的基础上,将一个系统中的不同业务场景进行拆分。每个业务使用单独的库。好处:降低单数据库服务的压力(物理存储、内存、IO等)、降低单机故障的影响面

3.2.2 垂直分表

即大表拆小表,将一张表中数据不同”字段“分拆到多张表中,比如商品库将商品基本信息、商品库存、卖家信息等分拆到不同库表中。考虑因素有将不常用的,数据较大,长度较长(比如text类型字段)的拆分到“扩展表“,表和表之间通过”主键外键“进行关联。好处:降低表数据规模,提升查询效率,也避免查询时数据量太大造成的“跨页”问题。

3.2 水平拆分

操作:

将总体数据按照某种维度(时间、用户)等分拆到多个库中或者表中,典型特征不同的库和表结构完全一下,如订单按照(日期、用户ID、区域)分库分表。

水平拆表 :

将数据按照某种维度拆分为多张表,但是由于多张表还是从属于一个库,其降低锁粒度,一定程度提升查询性能,但是仍然会有IO性能瓶颈。

水平拆库

将数据按照某种维度分拆到多个库中,降低单机单库的压力,提升读写性能。

3.3 常见水平拆分方案

3.3.1 Range分库分表

说明:

按日期将不同月甚至是日的数据分散到不同的库(或者表)中;将userId为1-300w的记录分到第一个库(或者表),300w-600w的分到第二个库(或者表),以此类推。某种意义上,某些系统中使用的"冷热数据分离",将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。

优点:

单表大小可控天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。

缺点:

热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询

3.3.2 hash分库分表

hash取模策略:

指定的路由key(一般是user_id、order_id、customer_no作为key)对分表总数进行取模,把数据分散到各个表中。比如原始订单表信息,我们把它分成4张分表:

比如id=1,对4取模,就会得到1,就把它放到t_order_1;id=3,对4取模,就会得到3,就把它放到t_order_3;

一般,我们会取哈希值,再做取余:

Math.abs(orderId.hashCode()) % table_number

优点:

hash取模的方式,不会存在明显的热点问题。

缺点:

如果未来某个时候,表数据量又到瓶颈了,需要扩容,就比较麻烦。所以一般建议提前规划好,一次性分够。(可以考虑一致性哈希)

如果一开始按照hash取模分成4个表了,未来某个时候,表数据量又到瓶颈了,需要扩容,这就比较棘手了。比如你从4张表,又扩容成8张表,那之前id=5的数据是在(5%4=1,即第一张表),现在应该放到(5%8=5,即第5张表),也就是说历史数据要做迁移了。

3.3.3 range+hash取模混合

既然range存在热点数据问题,hash取模扩容迁移数据比较困难,我们可以综合两种方案一起嘛,取之之长,弃之之短。

比较简单的做法就是,在拆分库的时候,我们可以先用range范围方案,比如订单id在0-4000万的区间,划分为订单库1,id在4000万-8000万的数据,划分到订单库2,将来要扩容时,id在8000万~1.2亿的数据,划分到订单库3。然后订单库内,再用hash取模的策略,把不同订单划分到不同的表。

3.3.4 一致性hash

五.分库分表带来的问题

4.1 事务性问题

分库可能导致执行一次事务所需的数据分布在不同服务器上,数据库层面无法实现事务性操作,需要更上层业务引入分布式事务操作,难免会给业务带来一定复杂性,那么要想解决事务性问题一般有两种手段:

方案一:在进行分库分表方案设计过程中,从业务角度出发,尽可能保证一个事务所操作的表分布在一个库中,从而实现数据库层面的事务保证。方案二:方式一无法实现的情况下,业务层引入分布式事务组件保证事务性

2PC3PCTCCSAGA本地消息表最大努力通知

4.2 主键(自增ID)唯一性问题

在数据库表设计时,经常会使用自增ID作为数据主键,这就导致后续在迁库迁表、或者分库分表操作时,会因为主键的变化或者主键不唯一产生冲突,要解决主键不唯一问题,有如下方案:

方案一:使用单独MySQL主键表产生自增id

方案二:Redis自增

4.3 跨库多表join问题

分库分表之后,两张表可能都不在同一个数据库中了,那么如何跨库join操作呢?

跨库Join的几种解决思路:

字段冗余:把需要关联的字段放入主表中,避免关联操作;比如订单表保存了卖家ID(sellerId),你把卖家名字sellerName也保存到订单表,这就不用去关联卖家表了。这是一种空间换时间的思想。全局表:比如系统中所有模块都可能会依赖到的一些基础表(即全局表),在每个数据库中均保存一份。数据抽象同步:比如A库中的a表和B库中的b表有关联,可以定时将指定的表做同步,将数据汇合聚集,生成新的表。一般可以借助ETL工具。应用层代码组装:分开多次查询,调用不同模块服务,获取到数据后,代码层进行字段计算拼装。

4.4 跨库聚合查询问题

4.4.1 分页查询

假设将订单表根据hash(uid%2+1)拆分成了两张表,如下图:

假设现在需要根据订单的时间进行排序分页查询(这里不讨论shardingKey路由,直接全表扫描),在单表中的SQL如下:

select * from t_order order by time asc limit 5 offset 5;

这条SQL非常容易理解,就是翻页查询第2页数据,每页查询5条数据,其中offest=5

假设现在t_order_1和t_order_2中的数据如下:

以上20条数据从小到大的排序如下:

t_order_1中对应的排序如下:

t_order_2中对应的排序如下:

那么单表结构下最终结果只需要查询一次,结果如下:

分表的架构下如何分页查询呢?下面介绍几种方案:

1. 全局查询法

在数据拆分之后,如果还是上述的语句,在两个表中直接执行,变成如下两条SQL:

select * from t_order_1 order by time asc limit 5 offset 5;

select * from t_order_2 order by time asc limit 5 offset 5;

将获取的数据然后在内存中再次进行排序,那么最终的结果如下:

可以看到上述的结果肯定是不对的。

所以正确的SQL改写成如下:

select * from t_order_1 order by time asc limit 0,10;

select * from t_order_2 order by time asc limit 0,10;

也就是说,要在每个表中将前两页的数据全部查询出来,然后在内存中再次重新排序,最后从中取出第二页的数据,这就是全局查询法

该方案的缺点非常明显:

随着页码的增加,每个节点返回的数据会增多,性能非常低服务层需要进行二次排序,增加了服务层的计算量,如果数据过大,对内存和CPU的要求也非常高

不过这种方案也有很多的优化方法,比如Sharding-JDBC中就对此种方案做出了优化,采用的是流式处理 + 归并排序的方式来避免内存的过量占用,有兴趣的可以自行去了解一下。

针对全局查询法的缺点,可以针对不同业务采取折中方案:

2.全局查询法折中方案一:禁止跳页查询法(适合下拉翻页)

数据量很大时,可以禁止跳页查询,只提供下一页的查询方法,比如APP或者小程序中的下拉翻页,这是一种业务折中的方案,但是却能极大的降低业务复杂度

比如第一页的排序数据如下:

那么查询第二页的时候可以将上一页的最大值1664088392作为查询条件,此时的两个表中的SQL改写如下:

select * from t_order_1 where time>1664088392 order by time asc limit 5;

select * from t_order_2 time>1664088392 order by time asc limit 5;

然后同样是需要在内存中再次进行重新排序,最后取出前5条数据

优点:不用返回前两页的全部数据了,只需要返回一页数据,在页数很大的情况下也是一样,在性能上的提升非常大缺点: 不能跳页查询,只能一页一页地查询,比如说从第一页直接跳到第五页,因为无法获取到第四页的最大值,所以这种跳页查询肯定是不行的。

3.全局查询法折中方案二:损失精度查询法(适合对精确性要求没那么高的业务)

数据库分库-数据均衡原理

使用patition key进行分库,在数据量较大,数据分布足够随机的情况下,各分库所有非patition key属性,在各个分库上的数据分布,统计概率情况是一致的。

例如,在uid随机的情况下,使用uid取模分两库,db0和db1:

(1)性别属性,如果db0库上的男性用户占比70%,则db1上男性用户占比也应为70%

(2)年龄属性,如果db0库上18-28岁少女用户比例占比15%,则db1上少女用户比例也应为15%

(3)时间属性,如果db0库上每天10:00之前登录的用户占比为20%,则db1上应该是相同的统计规律

利用这一原理,要查询全局100页数据,offset 9900 limit 100改写为offset 4950 limit 50,每个分库偏移4950(一半),获取50条数据(半页),得到的数据集的并集,基本能够认为,是全局数据的offset 9900 limit 100的数据,当然,这一页数据的精度,并不是精准的。

根据实际业务经验,用户都要查询第100页网页、帖子、邮件的数据了,这一页数据的精准性损失,业务上往往是可以接受的,但此时技术方案的复杂度便大大降低了,既不需要返回更多的数据,也不需要进行服务内存排序了。

4. 二次查找法

有没有一种方法既能满足业务要求,并且不需要折中,性能还高的方法呢?

因此方案相比前三个方案理解起来相对复杂点,为了方便说明,所以先单一DB说起,以下单一DB中保存用户年龄数据,1到30岁,总共30条,如果要查询

select * from T order by age limit 5 offset 10

当对多个数据库中的数据进行分页查询时,首先根据字段进行排序,一般是时间time,这里我们使用age进行排序(排序是二次查找法的关键点,保证最小值的offset一定小于全局offset)

那么会返回以下粉色标识数据,即【11-15】,请记住此结果,下面会讲解怎么分库查询以下结果。

把以上所有数据进行拆分打散存放到3个分库中,如下,注意下面数据只是用户属性年龄,不是分片键:

通过上文介绍,在单一DB中查询limit 5 offset 10,返回了【11-15】结果,那如果在以上三个分库全局查询limit 5 offset 10怎么做?

**关键点:找到各个分片结果集中的最小的一位在全局中的offset **

第一步:语句改写

将 select _ from T order by age limit 5 offset 10 改写为 select _ from T order by age limit 5 offset 3 , 并投递给所有的分库,注意,这个 offset 的 3,来自于全局offset的总偏移量 10,除以水平切分数据库个数 3。

执行select * from T order by age limit 5 offset 3,结果如下(粉色标识数据),为了便于理解用青黄色标识库表前三条数据:

第二步:找到返回数据的最小值

第一个库,5 条数据的 age 最小值是10;第二个库,5 条数据的 age 最小值是 6;第三个库,5 条数据的 age 最小值是 12;

故,三页数据中,age最小值来自第二个库,age_min=6,这个过程只需要比较各个分库第一条数据,时间复杂度很低

第三步:查询二次改写

第一次改写的SQL语句是select * from T order by age limit 5 offset 3 第二次要改写成一个between语句,between的起点是age_min,between的终点是原来每个分库各自返回数据的最大值:

第一个分库,第一次返回数据的最大值是22 所以查询改写为select * from T order by age where age between age_min and 22

第二个分库,第一次返回数据的最大值是20 所以查询改写为select * from T order by age where age between age_min and 20

第三个分库,第一次返回数据的最大值是25 所以查询改写为select * from T order by age where age between age_min and 25

相对第一次查询,第二次查询条件放宽了,故第二次查询会返回比第一次查询结果集更多的数据,假设这三个分库返回的数据如下:

可以看到:

分库一的结果集,比第一次多返回了1条数据,上图中深蓝色记录7

由于age_min来自原来的分库二,所以分库二的返回结果集和第一次查询相同,其实这次查询可以省掉

分库三的结果集,比第一次多返回了3条数据,上图中深蓝色记录8,9,11

第四步:找到age_min在全局的offset

在每个结果集中虚拟一个age_min记录,找到age_min在全局的offset

因为查询语句为 limit 5 offset 3 ,所以查询结果集中每个分库的第一条数据offset为4;

分库一中,根据第一次查询条件得出的10的offset是4,查询又返回了一条数据向前推进一位索引,故虚拟age_min在第一个库的offset是2

分库二没有数据变化所以age_min的offset=4

分库三中,根据第一次查询条件得出的12的offset是4,查询又返回了三条数据向前推进三位索引,故虚拟age_min在第三个库的offset是0

因此age_min的全局offset为:2+4+0=6

既然得到了age_min在全局的offset为6,就有了全局视野,根据第二次的结果集,就能够得到全局limit 5 offset 10的记录(下图黄色标识数据), 具体计算如下,各分库二次查询结果如下:

分库1:7、10、14、16、21、22

分库2:6、13、17、19、20

分库3:8、9、11、12、15、18、23、25

统一放到list排序后:【6、7、8、9、10、11、12、13、14、15、16、17、18、19、20、21、22、23、25】,得知最小值全局offset为6,最终结果要取offset 10 limit 5,那就10-6=4,把排序后结果,向后推移4位,然后再取5位,那就是【11、12、13、14、15】

优点:可以精确地返回业务所需数据,每次返回的数据量都非常小,不会随着翻页增加数据的返回量、

缺点:需要进行两次查询

4.4.2 order by,group by等聚合函数问题

跨节点的count,order by,group by以及聚合函数等问题,都是一类的问题,它们一般都需要基于全部数据集合进行计算。可以分别在各个节点上得到结果后,再在应用程序端进行合并。

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

相关推荐

欢乐球球什么时候公测 公测上线时间预告

t2627_2_2627_1:29.0 t1-t0:43.0 t2-t1:7.0 t3-t2:0.0 t4-t3:4.0 t5-t4:15.0 t6-t5:18.0 t7-t6:4.0 t8-t7:0.0 t9-t8:66.0 t10-t9:0.0 t11-t10:6.0 t12-t11:0.0 t13-t12:0.0 t14-t13:0.0 t15-t14:0.0 t16-t

07-17 分类 365bet网页版

ADSL安装与接入线路布设指南

信号分离器的连接 ADSL宽带接入目前主要有虚拟拨号、专线接入、ADSL+LAN三种接入方式。 ADSL虚拟拨号是其中最常见的接入方式,所谓虚拟拨号是

07-03 分类 365bet网页版

日本面积多少平方公里

日本,这个位于东亚的岛国,以其独特的文化、先进的科技和美丽的自然景观闻名于世。然而,对于许多人来说,日本面积多少平方公里可能是

06-30 分类 best365官网登录入口

光阴的意思

拼音guāng yīn 注音ㄍㄨㄤ 一ㄣ 繁体光陰 词性名词 近义时间 岁月 时光 年华 韶华 词语解释光阴[ guāng yīn ]⒈ 明亮与阴暗,白昼与黑夜。指日月

07-18 分类 365bet官网是多少

传奇世界 药品名称大全

增益药物篇 undefined在传奇世界中,增益药物是玩家提升战斗力、恢复生命的重要道具。以下是一些常见的增益药品: **1. 盘古神丹:** 巨大威力

07-04 分类 365bet官网是多少

明武宗,天启帝为什么不家族过继养子?

太白入月敌可摧2025-06-03 06:40:36发布于江苏点灭只看此人举报1楼天启没法过继。因为他只有一个弟弟信王(另外五个弟弟均夭折),信王刚结婚

07-21 分类 365bet网页版