文章目录程序员囧辉 大厂面试题之分库分表总结笔记
- 分库分表
- 什么是分库分表
- 拆分方式
- 为什么要分库分表
- 如何进行分库分表
- 何时分库分表
- 如何选择分库分表
- 十亿级数据库分库核心流程详解(*)
- 完整流程
- 拆分SOP
- 1.目标评估
- 2.切分策略
- 3.分表字段
- 4.资源准备、代码改造
- 5.增量数据同步(双写)
- 6.全(存)量数据迁移
- 7.数据一致性校验、优化、补偿(最重要,花时间最多)
- 8.灰度切读
- 9.databus切新库
- 10.下游切数据源
- 11.停写老库
- 相关工具
- 如何解决分库分表带来的问题
- 1.分布式唯一ID
- 2.分布式事务
- 3.保证数据的最终一致性
- 4.跨库JOIN/分页查询
分库:在表数量不变的情况下,对数据库进行拆分
分表:数据库数量不变的情况下,对数据库表进行拆分
分库分表:数据库的数量和表的数量都发生变更
拆分方式- 水平拆分:整个表数据结构不发生变化的情况下,将一张表的数据拆分成多张表,单张表数据记录越来越大时,这张表的查询跟写入性能相应越来越慢,通过拆表让每张表的数据变小,提供更优的读写性能
- 垂直拆分:将本来放在一张表的字段拆分到多张表中,随着业务的发展,某些字段可能越来越大影响基本信息的查询
单台mysql服务器的硬件资源时有限的,随着业务的不断发展,请求量和数据量会不断增加,数据库的压力越来越大,某时数据库的读写性能开始下降,数据库将变成请求链路中的一个瓶颈,需要对数据库进行优化,业务初期,使用增加索引、读写分离、增加从库的手段进行优化,随着数据量的不断增加,这些优化手段作用越来越小,此时使用分库分表进行优化,对数据进行切分,将单表和单库的数据量控制在一个合理的范围,保证高效的读写能力
如何进行分库分表 何时分库分表性能出现瓶颈,其他优化手段无法很好解决,分库分表作为最后的手段进行优化
-
单表出现瓶颈
- 单表数据量大,导致读写速度慢
-
单库出现瓶颈
- CPU压力过大(busy、load高),导致读写性能较慢
- 内存不足(缓存池命中低、磁盘读写IOPS较高)导致读写性能较慢
- 磁盘空间不足,导致无法正常写入数据
- 网络带宽不足,导致读写性能较慢
经过评估单库的容量和性能可以支持未来几年的增长
- 只分表
- 单表数据量较大,单表读写性能出现瓶颈
- 只分库
- 数据库(读)写压力大,数据库出现存储性能瓶颈
- 分库分表
- 单表数据量较大,单表读写性能出现瓶颈
- 数据库(读)写压力大,数据库出现存储性能瓶颈
分库分表核心内容——拆分sop
完整流程- 评估是否需要拆分
- 拆分详细技术方案
- 流程梳理及影响评估
- 方案选型
- 拆分SOP
- 稳定性保障
- 技术方案内部评审及优化
- 同步相关影响方
- 进行拆分
SOP:标准作业程序
1.目标评估评估:拆分几个库,几个表
目标:读写能力提升、负载降低、容量支持未来几年的发展
大多数情况下,将单表的行数作为一个重要的参考指标,控制在千万级以下
2.切分策略-
范围切分
- 优点:天然水平扩展。单表大小可控,后续扩容方便,无需进行数据迁移,可自动化处理
- 缺点:存在明显的写偏移,写流量基本都集中在最新的表上,没有起到将写流量均匀分摊到各库表的效果,读流量也可能存在偏移,最近增加的数据被查询的概率更大
-
中间表映射
- 将分表键和数据库的映射关系存放在单独的表上,每次路由前先查询这张表,得到具体的路由数据库和数据表,进行具体操作
- 优点:灵活
- 缺点:引入额外的单点,增加了流程复杂度,这个表会很大,查询的QPS会非常高,保证其高性能和高可用也是一个问题
-
哈希切分(主流方案)
- 通过对分键表进行一定的运算,通常取模,决定路由到哪个库和表
- 优点:数据分布比较均匀,不容易出现热点和并发访问的瓶颈
- 缺点:后续扩容需要迁移数据,存在跨节点查询的问题
核心思路:合理选择,尽量减少出现跨库、跨表查询
4.资源准备、代码改造资源准备:新集群的所需数据库资源可以尽早跟dba申请,特别拆分集群比较多的情况,一方面因为dba搭建新集群,花费一定时间,另一方面避免出现资源不足导致延期的情况。
代码改造:将新集群的数据源引入到我们的服务中,支持灵活的灰度读写切换,数据全量迁移和一致性校验等任务;因为整个分库分表的过程是不停机并且无损拆分,拆分过程中新老数据会同时存在一段时间,称为灰度,这段灰度期间,通过配置中心和相关规则灵活控制
核心流程:
- 数据库资源准备
- 分库分表规则配置
- 代码改造
- 写入:单写老库、双写、单写新库
- 读取:读老、读新、部分读老部分读新
- 灰度:指定门店灰度、比例灰度
写新库是为了后续切换到新库,因此新库必须有全部的数据,写老库是因为不确定拆分过程中是否存在问题,写老库保证有全部数据,万一新库流程有问题,及时切换到老库的流程,保证服务的高可用跟稳定性
作用:保证增量数据在新库和老库都有
方案:
- 同步双写:同步写新库和老库,所有写数据的地方进行修改成写两份数据,一般不会写全部的写逻辑,而是在底层通过aop的方式实现
- 异步双写:写老库,监听binlog异步同步到新库,自己动手
- 中间件同步工具:通过一定的规则将数据同步到目标库表,中间件团队帮你做
注意点:
- 写新库异常不能影响流程
作用:迁移老库的历史数据,保证新库有全量数据
方案:
- RD开发Job:查询老库数据,写入新库
- 中间件同步工具:通过一定的规则将数据同步到目标库表
注意点:
- 控制好同步速率
- 增量数据的并发问题
在全量数据迁移完毕,增量同步正常运行之后,还不能直接将流量切换到新库,还要验证数据的一致性,例如改造存在遗漏的地方,并发修改等导致数据不一致,并发下会出现很多不一致场景,是切读之前的最后一个保障,必须再三确认数据的正确,否则切读后导致线上问题
作用:确保新库数据正确,达到切读标准、检查是否存在改造遗漏点
方案:增量数据校验、全量数据校验、人工抽检
核心流程:
- 读取老库数据
- 读取新库数据
- 比较新老库数据,一致则继续比较下一条数据
- 不一致则进行补偿
- 新库存在,老库不存在:新库删除数据
- 新库不存在,老库存在:新库插入数据
- 新库存在,老库存在:比较所有字段,不一致则将新库更新为老库的数据
数据校验一致性通过后将部分流量切换到新数据库,灰度早期会先拿少量的门店进行灰度,观察一段时间后,没有问题再继续增加灰度的门店,后期逐步使用比例来进行灰度,直到最终将全部的流量都切换到新的数据库上
作用:开始将流量切到新库
原则:
- 有问题及时切回老库
- 灰度放量先慢后快,每次放量观察一段时间
- 支持灵活的规则:门店维护灰度、百(万)分比灰度
databus:监听binlog的组件,将读流量全部切换到新库后,此时新流程已经基本全部验证通过,开始为停写老库做准备,将监听的binlog从老库切换到新库
作用:使用新库的databus
核心流程:
- 启动新库databus,此时下游会同时收到新老库的binlog
- 观察一段时间是否正常
- 有问题及时关闭
- 没问题关闭老库的databus
注意点:
- 监听binlog的流程一般会收敛在团队内部,外部团队想监听binlog,一般使用封装过后的消息,这样在进行分库分表改造时对外部团队基本没有影响,改造更加方便
除了binglog外,主要的下游是数仓,数仓将商品数据定期同步到hive上,用于进行数据的相关操作,需要让数仓将数据源切换到新的数据源上
作用:确保下游迁移到新数据源,主要是数仓
数仓一般每天同步一次数据,因此在指定的时间内切换即可。对实时性的要求不高
11.停写老库原则:确认老库数据源全部迁移后,停写老库
至此,核心拆分流程结束,后续操作逐步将老数据库资源逐步下线
-
binlog监听工具
binlog是一个二进制文件,用于记录数据库表结构和表记录的变更,通过binlog文件,可知道数据库中哪些数据发生变更,从什么变成什么。binlog监听工具主要监听mysql产生的binlog,进行解析成我们易懂的格式,最后通过一定手段发送到下游,如消息队列
- Databus
- Canal
-
分库分表工具
-
增强版JDBC驱动(常见用法)
以客户端jar包形式提供对JDBC的封装,客户端直接连接数据库
- Sharding-JDBC、TDDL、Zebra
-
数据库代理
需要单独部署,客户端连接代理服务,代理服务负责和数据库打交道
- Sharding-Proxy、MyCat
-
单库单表时使用自增ID就可保证ID的唯一性,分库分表之后,一张表被拆分成多张表,自增ID无法保证唯一性,需要引入以下方案保证ID唯一性
-
UUID
- 优点:本地生成,性能高
- 缺点:
- 更占存储空间,一般为长度36的字符串
- 不适合作为MySQL主键,MySQL主键推荐使用单调递增的数字,主键使用的是聚簇索引,把相邻主键的数据放在相邻物理存储位置上,当主键单调递增时,只需简单的将数据追加到索引最后面即可
- 无序性会导致磁盘随机IO(将数据插入之前已有的数据中间,插入位置所在的数据页不在内存中,可能需要先从磁盘读取到内存中,导致磁盘随机IO)、叶分裂(数据页的空间不足,移动大量数据)等问题
- 普通索引需要存储主键值,如果主键索引的值更占用内存空间,导致普通索引B+树“变高”,IO次数变多
- 基于MAC地址生成的算法可能导致MAC地址泄露
-
雪花算法
核心思想:通过一定的规则生成一个64位的long类型数字,除了最高位的1位不用之外,其他63位由三部分组成
- 1bit:不用
- 41bit:时间戳,可用69年
- 10bit:工作机器id,可部署1024台服务器
- 12bit:序列号,每毫秒可生成4096个ID,每秒即为409万
-
号段模式
数据库生成的方式:使用一个额外表的自增ID来作为分布式ID,因为ID都是由同一张表生成,所以可以保证全局的唯一性,缺点是每次使用分布式唯一ID都需要来读写这张表,当并发量大时,数据库存在严重的性能问题
号段模式即在此基础上优化,将之前的每次获取分布式ID读写,优化成批量的方式,获取一批ID,放在本地缓存中,用完之后再申请下一批,大大降低数据库的读写压力
在分库分表之前,全部的表都在同一个库中,可以使用本地事务保证数据的正确性,引入分库分表之后,数据库表在不同的数据库中,无法使用本地事务,需要引入分布式事务保证数据的正确性
-
2PC(Two Phase Commitment)两阶段提交 应用层面的处理
-
核心思想:将事务分为两个阶段
-
第一阶段:协调者首先询问所有的事务参与者是否可以执行事务的提交操作
-
第二阶段:协调者根据所有参与者的返回结果决定是否提交操作
- 全部的参与者都返回成功,则协调者向所有参与者发送事务提交请求
- 否则协调者向所有参与者发送事务回滚请求
-
-
优点:整理流程比较简单
-
缺点:存在同步阻塞、协调者单点等问题
-
-
TCC(try confirm cancel) 数据库层面的处理
-
核心思想:针对每个操作都有一个对应的确定和取消操作
-
核心流程:
- 主服务调用所有从服务的try接口进行业务检查和资源预留
- 主服务根据所有从服务的返回结果决定是否提交事务
- 所有从服务都返回成功,则主服务调用所有从服务的confirm接口进行事务确认提交操作
- 否则主服务调用所有从服务的cancel接口进行事务取消并释放预留资源
-
三阶段提交、本地消息表、事务消息…
在实际的高并发业务中,一般不会使用强一致性的分布式事务,金融场景除外,更多通过一些手段保证最终一致性。常见手段有 回滚、重试、监控、告警、幂等、对账等等,终极手段人工补偿。
以外卖下单为例子:
整个用户下单的流程涉及到很多步骤,最核心的包括创建订单、扣减商品库存、核销优惠券、核销会员红包等等,如果其中有一环节失败,则导致整个下单流程失败,需要将其他的流程都进行回滚,以保证不会产生资损,否则可能用户下单失败,但是出现用户红包被扣掉的情况
为了避免网络抖动等情况导致回滚失败,一般会有回滚、重试的流程,但是重试一般有次数的上限(监控),如果重试多次还是失败,有可能是其他问题,比如代码bug。超过这个上限,还是回滚失败,则需要发送告警,人为介入排查,人工修复补偿这些数据。
对于订单的下游服务,例如库存和优惠券则需要做好接口的幂等,没有做好幂等则可能出现数据重复回滚的情况,造成数据的错误和资损。
广义上讲,保证最终一致性也属于分布式事务的一种,不使用强一致性的分布式事务来保证可能有以下原因:
- 带来严重的性能损耗,导致下单的流程耗时增加,最终导致服务吞吐量下降,用户下单体验变差
- 会引入额外的复杂度,开发和维护的成本比较高
- 实际业务中由于部分成功导致数据不一致的场景发生的概率比较低
总结就是一个取舍的问题,目前大部分业务场景使用强一致性分布式事务的ROI并不高,因此一般不会选择强一致性事务,而是选择柔性事务,保证数据的最终一致性
4.跨库JOIN/分页查询在单库单表时,全部数据都放一张表中,因此可以随意进行join和分页操作,但是如果进行分库分表,数据会分到不同的数据库和数据表上,导致原本进行分页的数据分到了不同的数据库中,导致跨库查询问题
目前业界主流的解决方案有以下几种:
-
合适的分表字段(sharding key)
分表字段的合理选择,要能保证绝大部分高频查询场景不会出现跨库查询问题
实际业务中,分表字段选择合理,可避免95%~99%的跨库查询问题
-
搜索引擎支持:ES
将全量的数据冗余一份到ES中。使用ES支持复杂查询
-
核心流程:
-
使用ES查询出关键字段
-
使用关键字段去数据库查询完整的数据
-
-
注意点:
-
ES只存储需要搜索的字段,控制ES的大小,避免ES过大,导致性能和存储问题
-
ES只用于支持数据库难以查询的查询场景,如跨库查询、复杂搜索查询,这种查询不会太多,保证ES的整体压力不会太大
-
-
-
分开查询,内存中聚合
和 join 大同小异,区别是 join是数据库来做这个聚合操作,分开查询是应用层面做聚合操作
不建议在数据库中使用 join操作,而是建议分开查询再去内存中聚合,因为数据库资源相对应用服务器更宝贵一点,且更容易成为链路中的瓶颈,因此尽量不要让数据库做太复杂的查询,避免占用更多的数据库资源
- 核心流程:
- 先查询A表的数据
- 根据A表的结果查询B表
- 注意点:
- 查询出来的数据量
- 占用内存情况
- 核心流程:
-
冗余字段
- 核心流程:A表查询需要B表的field1字段,则将B表的field1字段存储一份到A表上
- 注意点:适用于只需要少量字段,则可以直接冗余



