
本文从 5W1H 角度介绍了分库分表手段,其在解决如 IO 瓶颈、读写性能、物理存储瓶颈、内存瓶颈、单机故障影响面等问题的同时也带来如事务性、主键冲突、跨库 join、跨库聚合查询等问题。anyway,在综合业务场景考虑,正如缓存的使用一样,本着非必须勿使用原则。如数据库确实成为性能瓶颈时,在设计分库分表方案时也应充分考虑方案的扩展性,或者考虑采用成熟热门的分布式数据库解决方案,如 TiDB。
阅读此文你将了解:
对于后端程序员来说,绕不开数据库的使用与方案选型,那么随着业务规模的逐渐扩大,其对于存储的使用上也需要随之进行升级和优化。
随着规模的扩大,数据库面临如下问题:
面对上述问题,常见的优化手段有:

索引优化、主从同步、缓存、分库分表每个技术手段都可以作为一个专题进行讲解,本文主要介绍分库分表的技术方案实现。
对于阅读本文的读者来说,分库分表概念应该并不会陌生,其拆开来讲是分库和分表两个手段:
性能角度:CPU、内存、磁盘、IO 瓶颈
可用性角度:单机故障率影响面
如果是单库,数据库宕机会导致 100%服务不可用,N 库则可以将影响面降低 N 倍。
事务性问题
分库可能导致执行一次事务所需的数据分布在不同服务器上,数据库层面无法实现事务性操作,需要更上层业务引入分布式事务操作,难免会给业务带来一定复杂性,那么要想解决事务性问题一般有两种手段:
主键(自增 ID)唯一性问题
跨库多表 join 问题
跨库聚合查询问题
分库分表会导致常规聚合查询操作,如 group by,order by 等变的异常复杂。需要复杂的业务代码才能实现上述业务逻辑,其常见操作方式有:
§ 方案一:赛道赛马机制,每次从 N 个库表中查询出 TOP N 数据,然后在业务层代码中进行聚合合并操作。
§ 假设: 以2库1表为例,每次分页查询N条数据。
§
§ 第一次查询:
§ ① 每个表中分别查询出N条数据:
§ SELECT * FROM db1_table1 where $col > 0 order by $col LIMITT 0,N
§ SELECT * FROM db2_table1 where $col > 0 order by $col LIMITT 0,N
§ ② 业务层代码对上述两者做归并排序,假设最终取db1数据K1条,取db2数据K2条,则K1+K2 = N
§ 此时的DB1 可以计算出OffSet为K1 ,DB2计算出Offset为K2
§ 将获取的N条数据以及相应的Offset K1/K2返回给 端上。
§
§ 第二次查询:
§ ① 端上将上一次查询对应的数据库的Offset K1/K2 传到后端
§ ② 后端根据Offset构造查询语句查询分别查询出N条语句
§ SELECT * FROM db1_table1 where $col > 0 order by $col LIMITT $K1,N
§ SELECT * FROM db2_table1 where $col > 0 order by $col LIMITT $K2,N
§ ③ 再次使用归并排序,获取TOP N数据,将获取的N条数据以及相应的Offset K1/K2返回给 端上。
§
§ 第三次查询:
依次类推.......
§ 方案二:可以将经常使用到 groupby,orderby 字段存储到一个单一库表(可以是 REDIS、ES、MYSQL)中,业务代码中先到单一表中根据查询条件查询出相应数据,然后根据查询到的主键 ID,到分库分表中查询详情进行返回。2 次查询操作难点会带来接口耗时的增加,以及极端情况下的数据不一致问题。
满足业务场景需要:根据业务场景的不同选择不同分库分表方案:比如按照时间划分、按照用户 ID 划分、按照业务能力划分等
方案可持续性:
最小化数据迁移:扩容时一般涉及到历史数据迁移,其扩容后需要迁移的数据量越小其可持续性越强,理想的迁移前后的状态是(同库同表>同表不同库>同库不同表>不同库不同表)
数据偏斜:数据在库表中分配的均衡性,尽可能保证数据流量在各个库表中保持等量分配,避免热点数据对于单库造成压力。

垂直拆表
垂直拆库
操作:将总体数据按照某种维度(时间、用户)等分拆到多个库中或者表中,典型特征不同的库和表结构完全一下,如订单按照(日期、用户 ID、区域)分库分表。
水平拆表
水平拆库
顾名思义,该方案根据数据范围划分数据的存放位置。
举个最简单例子,我们可以把订单表按照年份为单位,每年的数据存放在单独的库(或者表)中。
时下非常流行的分布式数据库:TiDB 数据库,针对 TiKV 中数据的打散,也是基于 Range 的方式进行,将不同范围内的[StartKey,EndKey)分配到不同的 Region 上。
缺点:
例子:交易系统流水表则是按照天级别分表。
hash 分表是使用最普遍的使用方式,其根据“主键”进行 hash 计算数据存储的库表索引。原理可能大家都懂,但有时拍脑袋决定的分库分表方案可能会导致严重问题。
对于分库分表,最常规的一种思路是通过主键计算 hash 值,然后 hash 值分别对库数和表数进行取余操作获取到库索引和表索引。比如:电商订单表,按照用户 ID 分配到 10 库 100 表中。
const (
// DbCnt 库数量
DbCnt = 10
// TableCnt 表数量
TableCnt = 100
)
// GetTableIdx 根据用户 ID 获取分库分表索引
func GetTableIdx(userID int64) (int64, int64) {
hash := hashCode(userID)
return hash % DbCnt, hash % TableCnt
}
上述是伪代码实现,大家可以先思考一下上述代码可能会产生什么问题?
比如 1000? 1010?,1020 库表索引是多少?
思考一下........
思考一下........
思考一下........
思考一下........
思考一下........
思考一下........
答:数据偏斜问题。

非互质关系导致的数据偏斜问题证明:
假设分库数分表数最大公约数为a,则分库数表示为 m*a , 分表数为 n*a (m,n为正整数)
某条数据的hash规则计算的值为H,
若某条数据在库D中,则H mod (m*a) == D 等价与 H=M*m*a+D (M为整数)
则表序号为 T = H % (n*a) = (M*m*a+D)%(n*a)
如果D==0 则T= [(M*m)%n]*a
思路一中,由于库和表的 hash 计算中存在公共因子,导致数据偏斜问题,那么换种思考方式:10 个库 100 张表,一共 1000 张表,那么从 0 到 999 排序,根据 hash 值对 1000 取余,得到[0,999]的索引,似乎就可以解决数据偏斜问题:
// GetTableIdx 根据用户 ID 获取分库分表索引
// 例子:1123011 -> 1,1
func GetTableIdx(userID int64) (int64, int64) {
hash := hashCode(userID)
slot := DbCnt * TableCnt
return hash % slot % DbCnt, hash % slot / DbCnt
}
上面会带来的问题?
比如 1123011 号用户,扩容前是 1 库 1 表,扩容后是 0 库 11 表

扩展性问题证明。
某条数据的hash规则计算的值为H,分库数为D,分表数为T
扩容前:
分片序号K1 = H % (D*T),则H = M*DT + K1 ,且K1 一定是小于(D*T)
D1 = K1 % D
T1 = K1 / D
扩容后:
如果M为偶数,即M= 2*N
K2 = H% (2DT) = (2NDT+K1)%(2DT) = K1%(2DT) ,K1 一定小于(2DT),所以K2=K1
D2 = K2%(2D) = K1 %(2D)
T2 = K2/(2D) = K1 / (2D)
如果M为奇数,即M = 2*N+1
K2 = H%(2DT) = (2NDT +DT +K1)%(2DT) = (DT+K1)%(2DT) = DT + K1
D2 = K2 %(2D) = (DT+K1) % (2D)
T2 = K2 /(2D) = (DT+K1) / (2D)
结论:扩容后库序号和表序号都变化
思路二中整体思路正确,只是最后计算库序号和表序号的时候,使用了库数量作为影响表序号的因子,导致扩容时表序号偏移而无法进行。事实上,我们只需要换种写法,就能得出一个比较大众化的分库分表方案。
func GetTableIdx(userId int64){
//①算Hash
hash:=hashCode(userId)
//②分片序号
slot:=hash%(DbCnt*TableCnt)
//③重新修改二次求值方案
dbIdx:=slot/TableCnt
tblIdx:=slot%TableCnt
return dbIdx,tblIdx
}
从上述代码中可以看出,其唯一不同是在计算库索引和表索引时,采用 TableCnt 作为基数(注:扩容操作时,一般采用库个数 2 倍扩容),这样在扩容时,表个数不变,则表索引不会变。
可以做简要的证明:
某条数据的hash规则计算的值为H,分库数为D,分表数为T
扩容前:
分片序号K1 = H % (D*T),则H = M*DT + K1 ,且K1 一定是小于(D*T)
D1 = K1 / T
T1 = K1 % T
扩容后:
如果M为偶数,即M= 2*N
K2 = H% (2DT) = (2NDT+K1)%(2DT) = K1%(2DT) ,K1 一定小于(2DT),所以K2=K1
D2 = K2/T = K1 /T = D1
T2 = K2%T = K1 % T = T1
如果M为奇数,即M = 2*N+1
K2 = H%(2DT) = (2NDT +DT +K1)%(2DT) = (DT+K1)%(2DT) = DT + K1
D2 = K2 /T = (DT+K1) / T = D + K1/T = D + D1
T2 = K2 %T = (DT+K1) % T = K1 %T = T1
结论:
M为偶数时,扩容前后库序号和表序号都不变
M为奇数时,扩容前后表序号不变,库序号会变化。
由思路二启发,我们发现案例一不合理的主要原因,就是因为库序号和表序号的计算逻辑中,有公约数这个因子在影响库表的独立性。那么我们是否可以换一种思路呢?我们使用相对独立的 Hash 值来计算库序号和表序号呢?
func GetTableIdx(userID int64)(int64,int64){
hash := hashCode(userID)
return atoi(hash[0:4]) % DbCnt,atoi(hash[4:])%TableCnt
}
这也是一种常用的方案,我们称为基因法,即使用原分片键中的某些基因(例如前四位)作为库的计算因子,而使用另外一些基因作为表的计算因子。
在使用基因法时,要主要计算 hash 值的片段保持充分的随机性,避免造成严重数据偏斜问题。
按照索引的思想,可以通过分片的键和库表索引建立一张索引表,我们把这张索引表叫做“路由关系表”。每次查询操作,先去路由表中查询到数据所在的库表索引,然后再到库表中查询详细数据。同时,对于写入操作可以采用随机选择或者顺序选择一个库表进入写入。
那么由于路由关系表的存在,我们在数据扩容时,无需迁移历史数据。同时,我们可以为每个库表指定一个权限,通过权重的比例调整来调整每个库表的写入数据量。从而实现库表数据偏斜率调整。
此种方案的缺点是每次查询操作,需要先读取一次路由关系表,所以请求耗时可能会有一定增加。本身由于写索引表和写库表操作是不同库表写操作,需要引入分布式事务保证数据一致性,极端情况可能带来数据的不一致。
且索引表本身没有分库分表,自身可能会存在性能瓶颈,可以通过存储在 redis 进行优化处理。

思路五中,需要将全量数据存在到路由关系表中建立索引,再结合 range 分库分表方案思想,其实有些场景下完全没有必要全部数据建立索引,可以按照号段式建立区间索引,我们可以将分片键的区间对应库的关系通过关系表记录下来,每次查询操作,先去路由表中查询到数据所在的库表索引,然后再到库表中查询详细数据。

一致性 Hash 算法也是一种比较流行的集群数据分区算法,比如 RedisCluster 即是通过一致性 Hash 算法,使用 16384 个虚拟槽节点进行每个分片数据的管理。关于一致性 Hash 的具体原理这边不再重复描述,读者可以自行翻阅资料。
其思想和思路五有异曲同工之妙。
总结
本文从 5W1H 角度介绍了分库分表手段,其在解决如 IO 瓶颈、读写性能、物理存储瓶颈、内存瓶颈、单机故障影响面等问题的同时,也带来如事务性、主键冲突、跨库 join、跨库聚合查询等问题。anyway,在综合业务场景考虑,正如缓存的使用一样,非必须使用分库分表,则不应过度设计采用分库分表方案。如数据库确实成为性能瓶颈时,在设计分库分表方案时也应充分考虑方案的扩展性。或者说可以考虑采用成熟热门的分布式数据库解决方案,如 TiDB。
作者:tayroctang
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co
我正在尝试在Rails上安装ruby,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,
SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手
前言一般来说,前端根据后台返回code码展示对应内容只需要在前台判断code值展示对应的内容即可,但要是匹配的code码比较多或者多个页面用到时,为了便于后期维护,后台就会使用字典表让前端匹配,下面我将在微信小程序中通过wxs的方法实现这个操作。为什么要使用wxs?{{method(a,b)}}可以看到,上述代码是一个调用方法传值的操作,在vue中很常见,多用于数据之间的转换,但由于微信小程序诸多限制的原因,你并不能优雅的这样操作,可能有人会说,为什么不用if判断实现呢?但是if判断的局限性在于如果存在数据量过大时,大量重复性操作和if判断会让你的代码显得异常冗余。wxswxs相当于是一个独立