一次分库分表全过程记录

实际操刀过一次线上的分库分表,距离现在时间有点久了,现在想一想,发现还是有不少有意思的东西,所以来一个迟到的记录

I. 背景分析

当时主要负责商品评价这一块业务,评价信息单表存储,主要包括一些基本的评价信息,商品id,订单id,买家id和卖家id,当时DB量级已经在3-4亿了,经常出现一些慢SQL,考虑到量级的问题,所以着手分库分表

1. 分库分表

首先知道分库分表一般有两种方式,水平划分和垂直划分

在具体划分,可如下

  • 垂直分表:将大表拆分成小表,将表中一些不常用的信息拆分出去,避免跨页查询
  • 垂直分库:根据不同的业务进行划分,每个业务有自己独立的数据库,如商品有商品库,用户有用户库,店铺有店铺库,db与db之间物理or逻辑分离
  • 水平分表:根据表中某些数据行,定义某种映射规则,将不同的数据行分布到不同的表中(表的结构基本不变)
  • 水平分库分表:将水平分表拆分出来的分表,放入不同的库中

2. 业务场景分析

根据上面分库分表的理解,很容易得出一个期待的结果

  • 独立的评价库,这个库里面只保存评价相关的信息
  • 采用水平分表,将原表的数据,拆分到1024(这个分表数量可自定义)张分表中(如果拆分的较合理,则单表数据量在30-40w)

那么一个问题就是如何进行水平拆分了,而考虑这一点,则需要根据实际的场景出发,分析sql的case

  • 商品详情的评价查询:根据商品ID进行查询
  • 卖家管理端查询所有评价:根据卖家ID进行查询
  • 买家评价管理:根据买家ID进行查询
  • 订单列表的评价信息:根据订单ID进行查询

粗略可以设定两种分表方案:

方案一:上面四种场景,显然是根据商品ID查询的量最大(因为详情的流量最大),如果根据商品ID进行分表,那么一个商品的评价都在落在一个分表中,此时查询不会出现夸表,但是另外三中查询则会导致跨表;同样换成卖家ID则会导致其他三个查询会跨表

第二种方式,建立一张附表,只保存如下信息

1
2
3
4
5
rateId: 评价ID
itemId: 商品ID
sellerId: 卖家ID
buyerId: 买家ID
orderId: 订单ID

然后根据rateId进行分库分表,这样每次查询时,先通过附表,查询到对应的评价ID,然后拿rateId到分表去获取评价信息

3. 基本信息

在实际的操作之前,先给出当时的表信息(说明,数据库和表名和表结构为本文定义,与实际有差异)

define desc
数据库 mysql
存储引擎 innorDB
库名 rate
表名 ItemRate
量级 3-4亿
binlog 开启binlog消息,有下游业务方通过mq方式消费db的变更

表结构类似下表,干掉了很多与主题无关的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `ItemRate` (
`id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '评价id',
`orderId` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '订单id',
`buyerId` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '买家ID',
`sellerId` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '卖家ID',
`itemId` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '商品ID',
`comment` varchar(1000) NOT NULL DEFAULT '' COMMENT '评价内容',
`imgUrls` varchar(500) NOT NULL DEFAULT '' COMMENT '上传的图片',
`created` int(11) unsigned NOT NULL DEFAULT '0',
`updated` int(11) unsigned NOT NULL DEFAULT '0',
`info` tinyint(4) NOT NULL DEFAULT '0' COMMENT '其他信息,省略...',
PRIMARY KEY `id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='评价表';

II. 设计&实现

在实际操作之前,首先是选择如何分库分表的方案了,其次就是整个操作的步骤规划,当这两个搞定之后,在开始编码实现才是比较合适的

首先确定分4库1024张表 (组内大神的建议,合理分表规则后单表量级在10w级别,业务没有大变化时,短期内将无序再次分表)

1. 方案设计

前面业务场景的分析中,就给出了两种方式,一个是根据商品ID进行分表,一个是建立一个附表然后根据rateId进行分表,首先是分表规则,我们选择最简单的方式,根据id % 1024进行取模

接下来分析下两种的优劣

a. itemID分表方案

优点:

  • 相比与下一种而言,实现简单,无需附表,也不用考虑数据不一致问题
  • 可以支持详情评价列表复杂的排序逻辑(当时排序方案不是离线计算的,走的DB的排序方式)
  • 性能更优,少了一张附表的过渡

缺点:

  • 非itemId查询会跨表
  • hash不均,导致单表量较多(比如可能出现某些爆款商品就有几十万的评价)

b. rateId分表方案

优点:

  • 支持前面的几种查询场景,不过需要先查询出rateId
  • 单表数据量分布均匀

缺点:

  • 复杂的查询条件支持不友好,特别是排序,过滤等依赖到一些评价信息的场景
  • 插入和更新时,都需要同步更新附表,这个保证复杂性更高(即要满足多表的事务,需要自己实现失败回滚)
  • 每次查询都需要先通过附表查询rateIds,效率更低(公司内的数据库中间件不允许join查询)

c. 方案选择

通过实际分析,选择了根据itemId进行分表的方案,理由如下:

  • 业务逻辑简单,后续维护也方便
  • 线上的sql中,95%以上都是根据itemId进行查询
  • 上层有一层根据solr的搜索,因此根据buyerId,sellerId,orderId 可以走搜索,不需进行db的多表联合查询,且性能ok
  • 附表虽然结构简单,但是单表最终的数据量依然可能很大,在亿级已上性能怎样也不好保证

因此,最终选择的方式是:

1
2
3
4
5
6
7
db    : Rate000 ~ Rate003,  
table : ItemRate0000 ~ ItemRaet1023

其中映射规则:

tableIndex = itemId % 1024 ---> 分表名 如 ItemRate0122
dbIndex = itemId % 1024 / 256 ---> 分库名 如 Rate001

2. 步骤计划

整个迁移的步骤,有两种方式,一个是双写方案:

方案一:采用双写

  • 创建DB
  • 服务的sql改造,主要是所有的sql,都要有一个新的分库分表的方式
  • 线上开始双写(即新增一条评价时,即写入就得表,也写入新的表)
  • 全量将旧表数据导入到新表数据
  • 线上读流量切换到新表,验证是否有问题
  • 所有关注旧表binlog的业务,全部迁移到关注新表的binlog(这里需要dba支持,因为新表有1024张,不可能关注1024个topic,这里需要dba提供binlog的整合,不在本文重点,此处略过)
  • 关闭双写,所有写直接切到新表
  • 删除旧代码

方案二:基于db的binlog实现的异步同步方式

  • 创建DB
  • 服务的sql改造,主要是所有的sql,都要有一个新的分库分表的方式
  • 开启增量,将某个时刻开始之后所有更新的数据,通过异步方式增量同步到新表
  • 开始全量,将之前的数据全部导入到新表
  • 线上读切新表,验证服务是否有问题
  • 所有关注旧表binlog业务,全部迁移到关注新表binlog
  • 线上切写到新表
  • 增量完毕后,关闭增量
  • 删除旧代码

两种方案本质上没有太大的区别,其中第一种双写,需要自己来保障双写成功;而后面增量的方式,一般DBA这边会有较好的方案来实现增量同步

额外说一点, 在实际的场景中,推动接听旧表binlog的业务方迁移是一件不可控的操作(因为啥原因大家都懂得),所以当时实际的情况是切到新表后,还会进行反向同步,即将新表的数据又重新的写入到旧表中,因此这种场景要求方案二中,切写到新表时,需要先停写一段时间,等待增量同步完毕之后,关闭增量,然后开启反向同步脚本,然后再切写

因为停写,对业务会有影响,所以当时采取第一种方案,因为反向同步不再本文重点,所以如果不考虑反向同步的情况,上面两种方式没有什么区别

3. 注意事项

  • 全量迁移,如果dba没有提供相关工具的话,就只能自己写了,当时是找了一台机器,部署了个服务利用jdbcTemplate来扫表,实现全量数据插入
  • 双写时,需要注意,如果双写失败应该怎么办
  • 数据对账比较有必要

III. 小结

1. 分库分表

  • 一般而言,当量级达到qw时,就该考虑分库分表了,不同的业务有自己独立的库

  • 根据表中某些列进行分表,将大表数据拆分到分表中

  • 大表拆分为小表

2. 一般实现步骤

  • 创建DB
  • 服务的sql改造,主要是所有的sql,都要有一个新的分库分表的方式
  • 线上开始双写
  • 全量将旧表数据导入到新表数据
  • 线上读流量切换到新表,验证是否有问题
  • 关闭双写,所有写直接切到新表
  • 删除旧代码

IV. 其他

个人博客: 一灰灰Blog

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正

扫描关注

QrCode