共计 3483 个字符,预计需要花费 9 分钟才能阅读完成。
背景
前段时间因为业务须要,须要对外围库分库分表,迁徙了大略有 40 亿数据,在此记录以便之后再来看看这种计划的优劣。
写在后面
为什么要拆库拆表?
随着公司业务疾速倒退,数据库中的数据量猛增,拜访性能也变慢了,优化火烧眉毛。剖析一下问题呈现在哪儿呢?关系型数据库自身比拟容易成为零碎瓶颈,单机存储容量、连接数、解决能力都无限。当单表的数据量达到 1000W 或 100G 当前,因为查问维度较多,即便增加从库、优化索引,做很多操作时性能仍降落重大。
针对生产环境中呈现的这种状况,咱们通常有软硬两种形式去解决,“硬”指的是在硬件方面上进行进步,即咱们通常挂在嘴边的加存储、加 CPU 等,这种计划的老本很高(ps:有钱人疏忽),并且如果瓶颈不在硬件就很好受了。”软“指的是咱们在设计层去做宰割,行将打表打散,将压力大的库拆分(星星之火也能够燎原的)。
常见的几种拆表形式
分库分表包含分库和分表两个局部,在生产中通常包含:垂直分库、程度分库、垂直分表、程度分表四种形式。
咱们先来理解下垂直和程度的概念:
- “垂直”通常指的是将一个表依照字段分成多表,每个表存储其中一部分字段。
- ”程度“通常指的是不会扭转表构造,将数据依照肯定的规定划分到多处。
咱们晓得这个概念之后再来解释常见的四种分库分表形式:
- 垂直分表:将一个宽表的字段按拜访频次、是否是大字段的准则或者其它特定的规定拆分为多个表,这样既能使业务清晰,还能晋升局部性能。拆分后,尽量从业务角度防止联查,否则性能方面将得失相当。
- 垂直分库:将多个表按业务耦合松紧归类,别离寄存在不同的库,这些库能够散布在不同服务器,从而使拜访压力被多服务器负载,大大晋升性能,同时能进步整体架构的业务清晰度,不同的业务库可依据本身状况定制优化计划。然而它须要解决跨库带来的所有简单问题。
- 程度分库:将一个表的数据 (按数据行) 分到多个不同的库,每个库只有这个表的局部数据,这些库能够散布在不同服务器,从而使拜访压力被多服务器负载,大大晋升性能。它不仅须要解决跨库带来的所有简单问题,还要解决数据路由的问题(数据路由问题后边介绍)。
- 程度分表:将一个表的数据 (按数据行) 分到多个同一个数据库的多张表中,每个表只有这个表的局部数据,这样做能小幅晋升性能,它仅仅作为程度分库的一个补充优化。
一般来说,在零碎设计阶段就应该依据业务耦合松紧来确定垂直分库,垂直分表计划,在数据量及拜访压力不是特地大的状况,首先思考缓存、读写拆散、索引技术等计划。若数据量极大,且持续增长,再思考程度分库程度分表计划。这里咱们还要思考一个问题,对于已有的业务咱们如何从原来的单库单表平滑无损的去迁徙到新的分片库分片表呢?(读者能够思考下这个问题,这也是本篇的重点)。
分库分表后带来的问题
- 主键 id 唯一性。
- 分布式事务问题:在执行分库分表之后,因为数据存储到了不同的库上,数据库事务管理呈现了艰难。
- 跨库跨表的 join 问题:在执行了分库分表之后,难以避免会将本来逻辑关联性很强的数据划分到不同的表、不同的库上,这时,表的关联操作将受到限制,咱们无奈 join 位于不同分库的表。
后期筹备
收敛所有直连 DB 的状况
为什么会有这一项呢?因为在咱们的业务代码中,很难防止的因为种种的问题导致有些业务场景中是直连的 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 的依赖
因为咱们须要进行分库分表操作,所以对于原有的依赖老库 binlog 的中央也要进行相应革新。
梳理分片键是否都能够获取到
这个能够依据本人的业务看是否须要解决
SOP
肯定要制订具体的 SOP 且要严格执行
方案设计(单库单表到分片库表的切换)
计划一
阶段一:创立新库并同步老库数据到新库
- 依据 binlog 同步数据
阶段二:校验数据一致性
- 校验 binlog 同步数据的一致性
阶段三:业务切流量
- 业务代码关上开关,切换为读写新库并放量
计划二
阶段一:创立新库并同步数据到新库
阶段二:停服 && 校验数据一致性
阶段三:业务切流量
计划三
阶段一:双写阶段
双写分为几种场景,insert&&update&&delete
状况一:失常状况(没有失败,更新的记录存在新老库之中)
- Insert 业务方双写新库老库:新库老库失常插入数据,因为是分库分表,所以采纳全局惟一 id 来代替原来的自增 id,历史数据保留原有信息
- Update 对于新老库进行更新(只针对新库有记录)
- Delete 现有业务暂无硬删除,疏忽
状况二:异常情况(老库失败,新库失败)
老库失败(老库 Insert 失败,Update 失败)
- 因为双写是串行的,所以即便失败了也不须要思考
新库失败(新库 Insert 库失败,新库 Update 失败)
- 新库 Insert 失败,记录写库信息,发送失败弥补音讯,进行数据修复
- 新库 Update 失败,记录写库信息,发送失败弥补音讯,进行数据修复
状况三:失败音讯重试
- 对于新库写库失败的数据,进行重试,比照新老库中的数据,雷同则跳过,不雷同用老库数据笼罩更新新库数据(加锁),如果此时依然失败,从新推送到音讯队列
- 对于新库还未存在的数据进行更新时,依据更新信息从老库读取数据,而后插入到新库,此过程对新老库记录加锁保证数据的一致性
阶段二:迁徙数据(脚本)
同步老库数据到新库,采纳 insert ignore 避免新老数据抵触
阶段三:数据一致性保障
分批次比照新老库数据(脚本)
- 雷同,跳过
- 不同,老库笼罩新库(仅老库加锁即可)
阶段四:切读
- 灰度放量新库读
- 全量切读
阶段五:迁徙上游依赖
次要是 binlog 依赖
阶段六:停写老库
阶段七:回收资源 && 清理开关
计划四
阶段一:创立新库并同步数据到新库(开启老库到新库同步数据)
阶段二:校验数据一致性
阶段三:停服 Rename 毫秒级别
阶段四:业务切流量至新库 先切读 再切写
阶段五:开启新库到老库增量数据同步,保障新老数据增量是统一的
计划优缺点简略比拟
计划一 | 计划二 | 计划三 | 计划四 | |
---|---|---|---|---|
长处 | 操作绝对简略 | 操作绝对简略 | 1. 整个过程不停服平滑迁徙且无数据损失 2. 任何阶段零危险回滚。3. 上游有富余的工夫做迁徙。4. 不便读新库灰度。 | 1. 操作绝对简略 2. 上游有富余的工夫做迁徙。3. 业务侵入较少。 |
毛病 | 1. 切流量至新库之后若不合乎预期,期间产生的数据均为问题数据且问题数据无奈疾速复原。2. 依赖上游迁徙进度。3. 切换过程中写入失败的数据会失落。4. 上线后验证工夫短。5. 回滚后再次上线代价较大,以上几个问题有反复呈现的危险。 | 同 1 | 1. 业务侵入较大。2. 双写影响接口性能 | 1. 在切换过程中服务不可用 2. 验证工夫短 3. 老库到新库写切换不洁净会导致数据时序问题(老库写和新库写操作同一条数据时)4. 回滚后再次上线后,以上几个问题有反复呈现的危险。 |
总结
在适合的业务场景采纳不同的计划才是最好的。
关注咱们
欢送对本系列文章感兴趣的读者订阅咱们的公众号,关注博主下次不迷路~