共计 7088 个字符,预计需要花费 18 分钟才能阅读完成。
一、分表键的抉择
什么是分表键
分表键即 分库 / 分表字段 ,zebra 外面叫做 维度,是在程度拆分过程中用于生成拆分规定的数据表字段。Zebra 依据分表键的值将数据表程度拆分到每个物理分库中。
数据表拆分的首要准则,就是要尽可能找到数据表中的数据在业务逻辑上的主体,并确定大部分(或外围的)数据库操作都是围绕这个主体的数据进行,而后可应用该主体对应的字段作为分表键,进行分库分表。
业务逻辑上的主体,通常与业务的利用场景相干,上面的一些典型利用场景都有明确的业务逻辑主体,可用于分表键:
- 面向用户的互联网利用,都是围绕用户维度来做各种操作,那么业务逻辑主体就是用户,可应用用户对应的字段作为分表键;
- 侧重于卖家的电商利用,都是围绕卖家维度来进行各种操作,那么业务逻辑主体就是卖家,可应用卖家对应的字段作为分表键;
以此类推,其它类型的利用场景,大多也能找到适合的业务逻辑主体作为分表键的抉择。
如果的确找不到适合的业务逻辑主体作为分表键,那么能够思考上面的办法来抉择分表键:
- 依据数据分布和拜访的平衡度来思考分表键,尽量将数据表中的数据绝对平均地散布在不同的物理分库 / 分表中,实用于大量剖析型查问的利用场景(查问并发度大部分能维持为 1);
- 依照数字(字符串)类型与工夫类型字段相结合作为分表键,进行分库和分表,实用于日志检索类的利用场景。
留神:无论抉择什么拆分键,采纳何种拆分策略,都要留神拆分值是否存在热点的问题,尽量躲避热点数据来抉择拆分键。
留神:不肯定须要拿数据库主键当做分表键,也能够拿其余业务值当分表键。拿主键当分表键的益处是能够散列平衡,缩小热点问题。
多个分表键如何解决
大部分场景下,一张表的查问条件比拟繁多,只须要一个分表键即可;然而有的时候,业务必须要有多个分表键,没有方法归一成一个。此时个别有四种解决形式:
名词定义:
- 主分表键 = 主维度,在主维度上,数据可能增删改查;
- 辅助分表键 = 辅维度,在辅助维度上,只能进行数据查问
在主维度上全表扫描
因为 SQL 中没有主维度,所以在对辅助维度进行查问时,只能在所有的主维度的表进行查问一遍,而后聚合。目前 zebra 的并发粒度是在数据库级别的,也就是说如果分了 4 个库,32 张表,最终会以 4 个线程去并发查问 32 张表,最终把后果合并输入。
实用场景:辅助维度的查问申请的量很小,并且是经营查问,对性能要求不高
多维度数据进行冗余同步
主维度的数据,通过 binlog 的形式,同步到辅助维度一份。那么在查问辅助维度时,会落到辅助维度的数据上进行查问。
实用场景:辅助维度的查问申请的量也很可观,不能间接应用第一种全表扫描的形式
二维奇妙归一维
辅助维度其实有的时候也是主维度,比方在订单表 Order 中,OrderID 和 UserID 其实是一一对应的,Order 表的主维度是 UserID,OrderID 是辅助维度,然而因为 OrderID 其中的 6 位和 UserID 完全一致,也就是说,在 OrderID 中会把 UserID 打进去。
在路由的时候,如果 SQL 中带有 UserID,那么间接拿 UserID 进行 Hash 取模路由;如果 SQL 中带有的 OrderID 维度,那么取出 OrderID 中的 6 位 UserID 进行 Hash 取模路由,后果是统一的。
实用场景:辅助维度和主维度其实能够通过将主维度和辅助维度的值进行信息共享
建设索引表
对于辅助维度能够建一张辅助维度和主维度的映射表。
举例来说,表 A 有两个维度,主维度 a,辅助维度 b,目前只有主维度的一份数据。
此时,如果有 SQL:select * from A where b =?过去,那么势必会在主维度上进行全表扫描。
那么建一张新表 B_A_Index,外面就只有两个字段,a 和 b 的值,这张表能够分表,也能够不分表,倡议分表这张表的主维度就是 b。
所以能够先查:select a from B_A_Index where b = ?,取得到 a 的值,而后 查问 select * from A where a = 查问到的值 and b = ? 进行查问。
试用场景:主副维度是一一对应的。劣势是,无需数据冗余,只须要冗余一份索引数据。毛病是,须要业务进行稍微的革新。
二、分片数的抉择
zebra 中的程度拆分有两个档次:分库和分表。
表数目决策
个别状况下,倡议单个物理分表的容量不超过 1000 万行数据。通常能够预估 2 到 5 年的数据增长量,用估算出的总数据量除以总的物理分库数,再除以倡议的最大数据量 1000 万,即可得出每个物理分库上须要创立的物理分表数:
- (将来 3 到 5 年内总共的记录行数) / 单张表倡议记录行数(单张表倡议记录行数 = 1000 万)
表的数量不宜过多,波及到聚合查问或者分表键在多个表上的 SQL 语句,就会并发到更多的表上进行查问。举个例子,分了 4 个表和分了 2 个表两种状况,一种须要并发到 4 表上执行,一种只须要并发到 2 张表上执行,显然后者效率更高。
表的数目不宜过少,少的害处在于一旦容量不够就又要扩容了,而分库分表的库想要扩容是比拟麻烦的。个别倡议一次分够。
倡议表的数目是 2 的幂次个数,不便将来可能的迁徙。
库数目决策
- 计算公式:依照存储容量来计算 =(3 到 5 年内的存储容量)/ 单个库倡议存储容量(单个库倡议存储容量 <300G 以内)
DBA 的操作,个别状况下,会把若干个分库放到一台实例下来。将来一旦容量不够,要产生迁徙,通常是对数据库进行迁徙。所以库的数目才是最终决定容量大小。
最差状况,所有的分库都共享数据库机器。最优状况,每个分库都独占一台数据库机器。个别倡议一个数据库机器上寄存 8 个数据库分库。
三、分表策略的抉择
分表形式 | 解释 | 长处 | 毛病 | 试用场景 | 版本要求 |
---|---|---|---|---|---|
Hash | 拿分表键的值 Hash 取模进行路由。最罕用的分表形式。 | 数据量散列平衡,每个表的数据量大致相同 申请压力散列平衡,不存在拜访热点 | 一旦现有的表数据量须要再次扩容时,须要波及到数据挪动,比拟麻烦。所以个别倡议是一次性分够。 | 在线服务。个别均以 UserID 或者 ShopID 等进行 hash。 | 任意版本 |
Range | 拿分表键依照 ID 范畴进行路由,比方 id 在 1 -10000 的在第一个表中,10001-20000 的在第二个表中,顺次类推。这种状况下,分表键只能是数值类型。 | 1. 数据量可控,能够平衡,也能够不平衡. 2. 扩容比拟不便,因为如果 ID 范畴不够了,只须要调整规定,而后建好新表即可。 | 无奈解决热点问题,如果某一段数据拜访 QPS 特地高,就会落到单表上进行操作。 | 离线服务。 | 2.9.4 以上 |
工夫 | 拿分表键依照工夫范畴进行路由,比方工夫在 1 月的在第一个表中,在 2 月的在第二个表中,顺次类推。这种状况下,分表键只能是工夫类型。 | 扩容比拟不便,因为如果工夫范畴不够了,只须要调整规定,而后建好新表即可 | 1. 数据量不可控,有可能单表数据量特地大,有可能单表数据量特地小 2. 无奈解决热点问题,如果某一段数据拜访 QPS 特地高,就会落到单表上进行操作。 | 离线服务。比方线下经营应用的表、日志表等等 |
1. 依照 UserID 进行 Hash 分表,依据 UserID 进行查问
XML 格局
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="Order" generatedPK="id">
<shard-dimension dbRule="#UserID#.toInteger()%32" dbIndexes="order_test[0-31]"
tbRule="#UserID#.toInteger().intdiv(32)%32" tbSuffix="alldb:[0,1023]"
isMaster="true">
</shard-dimension>
</table-shard-rule>
</router-rule>
Order 表的 UserID 维度一共分了 32 个库,别离是 order_test0 到 order_test31。一共分了 1024 张表,表名分表是 Order0 到 Order1023,平均分到了 32 个库中,每个库 32 张表。
2. 依照 UserID 进行 Hash 分表,依据 UserID 和 ShopID 进行查问
XML 格局
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="Order" generatedPK="OrderID">
<shard-dimension dbRule="#UserID#.toInteger()%10000%32" dbIndexes="order_test[0-31]"
tbRule="(#UserID#.toInteger()%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
isMaster="true">
</shard-dimension>
<shard-dimension dbRule="#ShopID# == null ? SKIP : (#ShopID#.toInteger()%16)" dbIndexes="order_shop_test[0-15]"
tbRule="(#ShopID#.toInteger()).intdiv(16) %16" tbSuffix="alldb:[0,255]"
needSync="true"
isMaster="false">
</shard-dimension>
</table-shard-rule>
</router-rule>
Order 表的 ShopID 维度的数据是依据 UserID 的数据从新通过 binlog 同步了一份。该份数据分了 16 个库,别离是 order_shop_test0 到 order_shop_test15。一共分了 256 张表,每个库 16 张表,表名分表是 Order0 到 Order255。
其中 zebra 会负责依据该配置,把 UserID 维度的数据主动的同步到 ShopID 维度,背地是通过 binlog 形式,所以会有 needSync 的属性。** 然而,该性能目前曾经永久性停用,如果须要应用,请自行接入 DTS/DataBus。
3. 依据 UserID 进行 Hash 分表,依据 UserID 和 OrderID 进行查问
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="Order" generatedPK="OrderID">
<shard-dimension dbRule="#UserID#.toInteger()%10000%32" dbIndexes="order_test[0-31]"
tbRule="(#UserID#.toInteger()%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
isMaster="true">
</shard-dimension>
<shard-dimension dbRule="#OrderID#[13..16].toInteger() % 32" dbIndexes="order_test[0-31]"
tbRule="(#OrderID#[13..16].toInteger()).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
isMaster="false">
</shard-dimension>
</table-shard-rule>
</router-rule>
Order 表的 OrderID 维度的数据和 UserID 的数据是统一的,并没有冗余。然而因为 OrderID 中的 13 到 16 位就是 UserID,所以能够应用 UserID 的数据进行查问。实质上,OrderID 和 UserID 必定能一一对应,其实是一个维度。
4. 依据 UserID 进行 Hash 分表,依据 AddTime 汇总大表供线下经营查问
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="Order" generatedPK="OrderID">
<shard-dimension dbRule="#UserID#.toInteger()%10000%32" dbIndexes="order_test[0-31]"
tbRule="(#UserID#.toInteger()%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
isMaster="true">
</shard-dimension>
<shard-dimension dbRule="#AddTime# == null ? SKIP : 0" dbIndexes="order_one"
tbRule="#AddTime# == null ? SKIP : 0" tbSuffix="alldb:[]"
needSync="true"
isMaster="false">
</shard-dimension>
</table-shard-rule>
</router-rule>
Order 表的 AddTime 维度,其实是通过 binlog 的形式把 UserID 的数据汇总到了 order_one 这个库,表名为 Order 这个表。这个汇总的过程是主动的。needSync=true。
线上服务应用 ShardDataSource,对 UserID 的数据进行增删改查。经营服务在拜访经营库 order_one 时,无需应用 ShardDataSource,间接应用 GroupDataSource 进行拜访,仅做查问应用。
5. 依据 UserID 的 crc32 值进行 Hash 分表
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="Order" generatedPK="OrderID">
<shard-dimension dbRule="crc32(#UserID#.toInteger())%10000%32" dbIndexes="order_test[0-31]"
tbRule="(crc32(#UserID#.toInteger())%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
isMaster="true">
</shard-dimension>
</table-shard-rule>
</router-rule>
crc32 是目前 zebra 的一个内置函数。
内置 HASH 函数
- crc32(Object value)
- crc32(Object value,String encode)
- md5(String value)
6. 主维度依据 UserID 进行 Hash 分表,辅助维度依据 AddTime 进行工夫分表
<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
<table-shard-rule table="Order" generatedPK="OrderID">
<shard-dimension dbRule="#UserID#.toInteger()%10000%32" dbIndexes="order_test[0-31]"
tbRule="(#UserID#.toInteger()%10000).intdiv(32) %32" tbSuffix="alldb:[0,1023]"
isMaster="true">
</shard-dimension>
<shard-dimension dbRule="shardByMonth(#AddTime#,"yyyy-MM-dd","2017-01-01","2027-01-31",3,0) % 4" dbIndexes="order_time[0-3]"
tbRule="shardByMonth(#AddTime#,"yyyy-MM-dd","2017-01-01","2027-01-31",3,0).intdiv(4)% 10" tbSuffix="alldb:[0,39]"
needSync="true"
isMaster="false">
</shard-dimension>
</table-shard-rule>
</router-rule>
Order 表的 AddTime 维度,其实是通过 binlog 的形式把 UserID 的数据从新依据工夫进行分表的,上述例子中,一共分了 4 个库,40 张表,3 个月一张表,涵盖 10 年的量。
线上零碎间接应用 ShardDataSource 应用 UserID 维度,线下经营零碎也是用 ShardDataSource,如果带了 AddTime,都会落到 AddTime 维度的数据上进行查问。反对 >,<,>=,<=,between and 等范畴查问;不反对 Join。
内置工夫函数
- shardByMonth(“#CreateTime”,String timeFormat, String startTime, String end, int monthPerTable, int defaultTableIndex)
该函数是依据月份进行计算路由的。其中,startTime 和 endTime 表明起始工夫和完结工夫,monthPerTable 是指从起始工夫开始几个月一张表,defaultTableIndex 是指如果超出了起始工夫和完结工夫时到一张默认的表的 Index。