Zebra 分库分表总结
一、分表键的选择
分表键即**分库/分表字段,zebra里面叫做维度,**是在水平拆分过程中用于生成拆分规则的数据表字段。Zebra 根据分表键的值将数据表水平拆分到每个物理分库中。
**拆分主要原则:**需要找到数据库表中的数据在业务逻辑上的主体,并确定大部分或者核心的数据库操作都围绕着这个主体的数据进行。可以使用该主体对应的字段作为分表键,进行分库分表。
业务逻辑上的主体,通常与业务的应用场景相关,下面的一些典型应用场景都有明确的业务逻辑主体,可用于分表键:
- 面向用户的互联网应用,都是围绕用户维度来做各种操作,那么业务逻辑主体就是用户,可使用用户对应的字段作为分表键;
- 侧重于卖家的电商应用,都是围绕卖家维度来进行各种操作,那么业务逻辑主体就是卖家,可使用卖家对应的字段作为分表键;
**注意:**无论选择什么拆分键,采用何种拆分策略,都要注意拆分值是否存在热点的问题,尽量规避热点数据来选择拆分键。
1.1.多个分表键如何处理
名词解释:
- **主维度:**主分表键,在主维度上,数据能够增删改查;
- 辅维度:辅助分表键,在辅助维度上,只能进行数据查询
大部分场景下,一张表的查询条件比较单一,只需要一个分表键即可;但是有的时候,业务必须要有多个分表键,没有办法归一成一个。此时一般以下有四种处理方式
1.1.1.在主维度上全表扫描
由于SQL中没有主维度,所以在对辅助维度进行查询时,只能在所有的主维度的表进行查询一遍,然后聚合。目前zebra的并发粒度是在数据库级别的,也就是说如果分了4个库,32张表,最终会以4个线程去并发查询32张表,最终把结果合并输出。
适用场景:辅助维度的查询请求的量很小,并且是运营查询,对性能要求不高
1.1.2.多维度数据进行冗余同步
主维度的数据,通过binlog的方式,同步到辅助维度一份。那么在查询辅助维度时,会落到辅助维度的数据上进行查询。
适用场景:辅助维度的查询请求的量也很可观,不能直接使用第一种全表扫描的方式
1.1.3.二维巧妙归一维
辅助维度其实有的时候也是主维度,比如在订单表Order中,OrderID和UserID其实是一一对应的,Order表的主维度是UserID,OrderID是辅助维度,但是由于OrderID其中的6位和UserID完全一致,也就是说,在OrderID中会把UserID打进去。
在路由的时候,如果SQL中带有UserID,那么直接拿UserID进行Hash取模路由;如果SQL中带有的OrderID维度,那么取出OrderID中的6位UserID进行Hash取模路由,结果是一致的。
适用场景:辅助维度和主维度其实可以通过将主维度和辅助维度的值进行信息共享
1.1.4.建立索引表
对于辅助维度可以建一张辅助维度和主维度的映射表。举例来说,表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 = ? 进行查询。
适用场景:主副维度是一一对应的。
**优势:**无需数据冗余,只需要冗余一份索引数据。
**缺点:**需要业务进行略微的改造。
总结:
需要找到业务数据库操作的核心主体,大多数情况下一个主维度能满足需求。如果实在不行,可以在辅维度同步一份数据,这样会造成数据冗余。
1.2.ugc分库分表键的选择
ugc线上服务查询条件统计:
可以看到:
- ugc_driver表大多数情况下使用driver_id作为查询条件,ugc_passenger大多数情况下使用passenger_id作为查询条件
- 原来通过主键ID作为查询条件的无法实现,需要改造
- 少数情况下查询ugc_driver表只知道passenger_id,如何改造??
解决方案:
- 原来传入id查询的接口进行改造,传入driverId/passengerId(需要和调用方协商,能否获取到这些参数)
- 思考业务,乘客和司机是通过订单关联到一起的。一笔订单有一个唯一的司机ID和一个唯一的乘客ID,通过订单查询接口获取需要的参数。
二、分片数的选择
2.1.表数目的决策
一般情况下,单个物理分表的容量不超过1000万行数据,通常预估未来2~5年的数据增长量。总数据库除以物理分库数,再除以建议的最大数据量1000w,可以得到每个物理分库上需要创建的物理分表数。
物理分表数 = 2~5年的数据总量 / 物理分库数 / 单表最大数据量
表的数量不宜过多,涉及到聚合查询或者分表键在多个表上的SQL语句,就会并发到更多的表上进行查询。
表的数目不宜过少,少的坏处在于一旦容量不够就又要扩容了,而分库分表的库想要扩容是比较麻烦的。一般建议一次分够。
建议表的数目是2的幂次个数,方便未来可能的迁移
2.2.库数目决策
计算公式:按照存储容量来计算 = (3到5年内的存储容量)/ 单个库建议存储容量 (单个库建议存储容量 <300G以内)
DBA的操作,一般情况下,会把若干个分库放到一台实例上去。未来一旦容量不够,要发生迁移,通常是对数据库进行迁移。所以库的数目才是最终决定容量大小。
最差情况,所有的分库都共享数据库机器。最优情况,每个分库都独占一台数据库机器。一般建议一个数据库机器上存放8个数据库分库。
2.3.ugc库表数目决策
三、分表策略的选择
分表方式 | 解释 | 优点 | 缺点 | 适用场景 | 版本要求 |
---|---|---|---|---|---|
Hash | 拿分表键的值Hash取模进行路由。最常用的分表方式。 | * 数据量散列均衡,每个表的数据量大致相同。* 请求压力散列均衡,不存在访问热点 | 一旦现有的表数据量需要再次扩容时,需要涉及到数据移动,比较麻烦。所以一般建议是一次性分够。 | 在线服务。一般均以UserID或者ShopID等进行hash。 | 任意版本 |
Range | 拿分表键按照ID范围进行路由,比如id在1-10000的在第一个表中,10001-20000的在第二个表中,依次类推。这种情况下,分表键只能是数值类型。 | * 数据量可控,可以均衡,也可以不均衡* 扩容比较方便,因为如果ID范围不够了,只需要调整规则,然后建好新表即可。 | 无法解决热点问题,如果某一段数据访问QPS特别高,就会落到单表上进行操作。 | 离线服务。 | 2.9.4以上 |
时间 | 拿分表键按照时间范围进行路由,比如时间在1月的在第一个表中,在2月的在第二个表中,依次类推。这种情况下,分表键只能是时间类型。 | * 扩容比较方便,因为如果时间范围不够了,只需要调整规则,然后建好新表即可。 | * 数据量不可控,有可能单表数据量特别大,有可能单表数据量特别小* 无法解决热点问题,如果某一段数据访问QPS特别高,就会落到单表上进行操作。 | 离线服务。比如线下运营使用的表、日志表等等 | 2.9.4以上 |
分表策略的选择取决于业务特性,ugc采用hash分表方式。
四、分库分表规则原理
请阅读官方文档:1. 分库分表规则原理及自定义配置
五、分库分表流程
参考UGC迁移,一个通用的流程是:
双写的实现方式有多种,推荐采用databus的方式,对业务逻辑侵入最小。
双写过程中遇到老库更新操作怎么办?
- 历史数据迁移过程中有update
代码块
SQL
insert ignore into ugc_passenger0.ugc_driver_comment_passenger_0(order_id,passenger_id,driver_id,car_type,`level`,tag_ids,extra_context,ctime)
select new_order_id,passenger_id,diver_id,car_type,`level`,tag_ids,extra_context,from_unixtime(create_time/1000,"%Y-%m-%d %H:%i:%S")
from ugc_passenger where id between 1 and 2000 and id not in(select id from test_id where id between 1 and 2000) and mod(passenger_id,4)=0
and floor(mod((passenger_id/4),128))=0 lock in share mode;
SELECT ... LOCK IN SHARE MODE走的是IS锁(意向共享锁)。通过共享锁保证数据一致性。insert into from a select from b lock in share mode 在执行时会锁住b表的行,直到处理完成。
- 实时数据双写遇到update,如何保证顺序执行
databus顺序保证。需要保证一条消息只会落到一个partition上
如果分库分表遇到服务切换怎么办
- 新服务中设置切读开关,第三步骤中新服务切读到新表,写还在老表
- 新服务中设置切写开关,第四步骤新服务中切写到新表
- 为了在出现问题时可回滚,需要有新库->老库的数据同步逻辑(血的教训)
六、zebra分库分表配置
待续