【业务学习】分库分表回顾
背景
前段时间因为业务需要,需要对核心库分库分表,迁移了大概有40亿数据,在此记录以便之后再来看看这种方案的优劣。
写在前面
为什么要拆库拆表?
随着公司业务快速发展,数据库中的数据量猛增,访问性能也变慢了,优化迫在眉睫。分析一下问题出现在哪儿呢? 关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能仍下降严重。
针对生产环境中出现的这种情况,我们通常有软硬两种方式去处理,“硬”指的是在硬件方面上进行提高,即我们通常挂在嘴边的加存储、加CPU等,这种方案的成本很高(ps:有钱人忽略),并且如果瓶颈不在硬件就很难受了。”软“指的是我们在设计层去做分割,即将打表打散,将压力大的库拆分(星星之火也可以燎原的)。
常见的几种拆表方式
分库分表包括分库和分表两个部分,在生产中通常包括:垂直分库、水平分库、垂直分表、水平分表四种方式。
我们先来了解下垂直和水平的概念:
- “垂直”通常指的是将一个表按照字段分成多表,每个表存储其中一部分字段。
- ”水平“ 通常指的是不会改变表结构,将数据按照一定的规则划分到多处。
- 垂直分表:将一个宽表的字段按访问频次、是否是大字段的原则或者其它特定的规则拆分为多个表,这样既能使业务清晰,还能提升部分性能。拆分后,尽量从业务角度避免联查,否则性能方面将得不偿失。
- 垂直分库:将多个表按业务耦合松紧归类,分别存放在不同的库,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能,同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案。但是它需要解决跨库带来的所有复杂问题。
- 水平分库:将一个表的数据(按数据行)分到多个不同的库,每个库只有这个表的部分数据,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能。它不仅需要解决跨库带来的所有复杂问题,还要解决数据路由的问题(数据路由问题后边介绍)。
- 水平分表:将一个表的数据(按数据行)分到多个同一个数据库的多张表中,每个表只有这个表的部分数据,这样做能小幅提升性能,它仅仅作为水平分库的一个补充优化。
分库分表后带来的问题
- 主键 id 唯一性。
- 分布式事务问题:在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。
- 跨库跨表的 join 问题:在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上,这时,表的关联操作将受到限制,我们无法join位于不同分库的表。
为什么会有这一项呢?因为在我们的业务代码中,很难避免的因为种种的问题导致有些业务场景中是直连的DB的,如果是自己团队的还好,如果不是就会造成不可预知的后果。这部分工作如果是工程相对规范且DB监控做的比较好的情况下还比较好排查,否则将很难去梳理全面,所以监控和规范不是没有用的,如果你说没有,拆库拆表试试吧。
收敛除了维护比较好维护之外,业务方对于自己的数据掌控度也比较大,所有的数据写入与读取都有明确的记录(想象一下自己维护的数据不知道被谁偷偷改了的烦恼)。
分布式ID生成器
我们采用分库分表,最佳的实现方式是在不同的分片表中使用全局唯一id。到这有的同学会问了,“为什么呢?我即使分库分表后,每条数据拆分到每个表中,由于MySQL数据库主键自增的缘故,它们的ID在各个表是独立的,查询的时候 select * from 表名,也能够查询出来对应的信息,欸,这也不需要唯一性ID啊“。但我们换个角度考虑,如:电商场景订单量巨大,订单数据存入数据库,肯定需要对数据库进行了分库分表,欸,你有没有发现每个人的订单号肯定都是不同的,这就体现了全局唯一性ID,当然同学又会说,我再开一个字段去单独存储这个订单id不行么?这就是要刚我啊,少侠手下留情。详见:ID生成器详解
梳理ID类型变更对依赖方的影响
既然我们采用了全局唯一id,我们就不得不考虑对依赖方的影响,大概有以下几点:
- 下游依赖有没有对库表的id进行强制转换类型,例如强制转化为int32。
- 前端是否有直接读取整形的ID,因为Javascript的数字存储使用了IEEE 754中规定的双精度浮点数数据类型,而这一数据类型能够安全存储 -(2^53-1) 到 2^53-1 之间的数值(包含边界值)。JSON 是 Javascript 的一个子集,所以它也遵守这个规则。而int64 类型的数值范围是 -(2^63-1) 到 2^63-1。使用int64 类型json 对于超出范围的数字,会出现解析错误的情况。
因为我们需要进行分库分表操作,所以对于原有的依赖老库binlog的地方也要进行相应改造。
梳理分片键是否都可以获取到
这个可以根据自己的业务看是否需要处理
SOP
一定要制定详细的SOP且要严格执行
方案设计(单库单表到分片库表的切换) 方案一
阶段一:创建新库并同步老库数据到新库
- 根据binlog同步数据
- 校验binlog同步数据的一致性
- 业务代码打开开关,切换为读写新库并放量
阶段一:创建新库并同步数据到新库 阶段二:停服&&校验数据一致性 阶段三:业务切流量 方案三
阶段一: 双写阶段 双写分为几种场景,insert&&update&&delete
- 情况一:正常情况(没有失败,更新的记录存在新老库之中)
- Insert 业务方双写新库老库: 新库老库正常插入数据,因为是分库分表,所以采用全局唯一id来代替原来的自增id,历史数据保留原有信息
- Update 对于新老库进行更新(只针对新库有记录)
- Delete 现有业务暂无硬删除,忽略
- 情况二: 异常情况(老库失败,新库失败)
- 老库失败(老库Insert失败,Update失败)
- 因为双写是串行的,所以即使失败了也不需要考虑
- 新库失败(新库Insert库失败,新库Update失败)
- 新库Insert失败,记录写库信息,发送失败补偿消息,进行数据修复
- 新库Update失败,记录写库信息,发送失败补偿消息,进行数据修复
- 情况三:失败消息重试
- 对于新库写库失败的数据,进行重试,对比新老库中的数据,相同则跳过,不相同用老库数据覆盖更新新库数据(加锁),如果此时仍然失败,重新推送到消息队列
- 对于新库还未存在的数据进行更新时,根据更新信息从老库读取数据,然后插入到新库,此过程对新老库记录加锁保证数据的一致性
- 老库失败(老库Insert失败,Update失败)
阶段三:数据一致性保证 分批次对比新老库数据(脚本)
- 相同, 跳过
- 不同,老库覆盖新库(仅老库加锁即可)
- 灰度放量新库读
- 全量切读
阶段六: 停写老库 阶段七: 回收资源&&清理开关 方案四
阶段一:创建新库并同步数据到新库(开启老库到新库同步数据) 阶段二:校验数据一致性 阶段三:停服Rename 毫秒级别 阶段四:业务切流量至新库 先切读 再切写 阶段五:开启新库到老库增量数据同步,保证新老数据增量是一致的 方案优缺点简单比较
方案一 | 方案二 | 方案三 | 方案四 | |
---|---|---|---|---|
优点 | 操作相对简单 | 操作相对简单 | 1. 整个过程不停服平滑迁移且无数据损失 2. 任何阶段零风险回滚。3. 下游有充裕的时间做迁移。4. 方便读新库灰度。 | 1. 操作相对简单2. 下游有充裕的时间做迁移。3. 业务侵入较少。 |
缺点 | 1. 切流量至新库之后若不符合预期,期间产生的数据均为问题数据且问题数据无法快速恢复。2. 依赖下游迁移进度。3. 切换过程中写入失败的数据会丢失。4. 上线后验证时间短。5. 回滚后再次上线代价较大,以上几个问题有重复出现的风险。 | 同1 | 1. 业务侵入较大。2. 双写影响接口性能 | 1. 在切换过程中服务不可用2. 验证时间短3. 老库到新库写切换不干净会导致数据时序问题(老库写和新库写操作同一条数据时)4. 回滚后再次上线后,以上几个问题有重复出现的风险。 |
关注我们 【【业务学习】分库分表回顾】欢迎对本系列文章感兴趣的读者订阅我们的公众号,关注博主下次不迷路~
文章图片
推荐阅读
- 宽容谁
- 我要做大厨
- 增长黑客的海盗法则
- 画画吗()
- 2019-02-13——今天谈梦想()
- 远去的风筝
- 三十年后的广场舞大爷
- 叙述作文
- 20190302|20190302 复盘翻盘
- 学无止境,人生还很长