24分库分表和冷热分离
第01章 高性能架构模式
互联网业务兴起之后,海量用户加上海量数据的特点,单个数据库服务器已经难以满足业务需要,必须考虑数据库集群的方式来提升性能。
高性能数据库集群的:
1、第一种方式是**“读写分离”**
2、第二种方式是**“数据库分片”**。
1 读写分离架构
读写分离原理: 读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是其基本架构图:
读写分离的基本实现:
主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
读写分离是根据 SQL 语义的分析,将读操作和写操作分别路由至主库与从库。
通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。
使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。
下图展示了根据业务需要,将用户表的写操作和读操路由到不同的数据库的方案:
2 数据库分片架构
读写分离的问题:
读写分离分散了数据库读写操作的压力,但没有分散存储压力,为了满足业务数据存储的需求,就需要将存储分散到多台数据库服务器上。
数据分片:
将存放在单一数据库中的数据分散地存放至多个数据库或表中,以达到提升性能瓶颈以及可用性的效果。 数据分片的有效手段是对关系型数据库进行分库和分表。数据分片的拆分方式又分为垂直分片和水平分片。
2.1 垂直分片
垂直分库:
按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。 在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。
垂直拆分可以缓解数据量和访问量带来的问题,但无法根治。如果垂直拆分之后,表中的数据量依然超过单节点所能承载的阈值,则需要水平分片来进一步处理。
垂直分表:
垂直分表适合将表中某些不常用的列,或者是占了大量空间的列拆分出去。
假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。
垂直分表引入的复杂性主要体现在表操作的数量要增加。例如,原来只要一次查询就可以获取 name、age、sex、nickname、description,现在需要两次查询,一次查询获取 name、age、sex,另外一次查询获取 nickname、description。
2.2 水平分片
水平分片又称为横向拆分。 相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中**,**每个分片仅包含数据的一部分。 例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表),如下图所示。
阿里巴巴Java开发手册:
【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
3 实现方式
Apache ShardingSphere(程序级别和中间件级别)
MyCat(数据库中间件)
第02章 ShardingSphere
1、简介
官网:https://shardingsphere.apache.org/index_zh.html
文档:https://shardingsphere.apache.org/document/5.1.1/cn/overview/
Apache ShardingSphere 由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。
2、ShardingSphere-JDBC
程序代码封装
定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
3、ShardingSphere-Proxy
中间件封装
定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。 目前提供 MySQL 和 PostgreSQL版本,它可以使用任何兼容 MySQL/PostgreSQL 协议的访问客户端(如:MySQL Command Client, MySQL Workbench, Navicat 等)操作数据,对 DBA 更加友好。
第03章 MySQL主从同步
一、Binlog基础
1. 什么是Binlog
Binlog(Binary Log)是MySQL Server层生成的二进制日志,记录了数据库中所有对数据的修改操作,包括INSERT、UPDATE、DELETE等DML操作,以及CREATE、ALTER等DDL操作。MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件,
binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志,用于备份恢复、主从复制;
2. Binlog的三种格式
| 格式 | 说明 | 适用场景 | 优缺点 |
|---|---|---|---|
| STATEMENT | 记录执行的SQL语句 | 简单操作、不涉及函数 | 优点:日志文件小;缺点:某些函数可能导致主从不一致 |
| ROW | 记录数据行的变化(修改前后的值) | 涉及函数、存储过程、非确定性操作 | 优点:数据一致性好;缺点:日志文件大 |
| MIXED | 自动选择STATEMENT或ROW | 大多数场景 | 优点:灵活;缺点:复杂 |
3. Binlog的关键作用
- 数据备份与恢复:通过binlog可以将数据库恢复到特定时间点
- 主从复制:主服务器将binlog同步到从服务器,实现数据一致性
- 数据审计:提供数据库操作历史的详尽记录
- 数据同步:支持多级复制、级联复制等架构
二、MySQL主从复制原理
1. 核心原理
主从复制是基于二进制日志的机制,实现数据从主服务器(Master)到从服务器(Slave)的同步。整个过程依赖于”三个线程”和”两种日志”:
三个线程:
- Binlog Dump线程(主服务器):负责读取主服务器的binlog并发送给从服务器
- I/O Thread(从服务器):从主服务器获取binlog并写入中继日志(relay log)
- SQL Thread(从服务器):执行relay log中的操作,实现数据同步
两种日志:
- binlog(主服务器):真实业务操作的”事实记录”
- relay log(从服务器):从服务器本地的缓存日志,用来重放
📌 关键点:主库commit = binlog写成功。这意味着binlog是主库事务成功的唯一证据。
1、MySQL主从同步原理

基本原理:
slave会从master读取binlog来进行数据同步
MySQL的二进制日志(Binary Log,简称Binlog)是MySQL数据库引擎提供的一种记录数据库变更的日志文件。它记录了对MySQL数据库进行的所有写操作,包括插入、更新、删除等,但不包括查询操作
具体步骤:
step1:master将数据改变记录到二进制日志(binary log)中。
step2:当slave上执行 start slave 命令之后,slave会创建一个IO 线程用来连接master,请求master中的binlog。
step3:当slave连接master时,master会创建一个 log dump 线程,用于发送 binlog 的内容。在读取 binlog 的内容的操作中,会对主节点上的 binlog 加锁,当读取完成并发送给从服务器后解锁。
step4:IO 线程接收主节点 binlog dump 进程发来的更新之后,保存到 中继日志(relay log) 中。
step5:slave的SQL线程,读取relay log日志,并解析成具体操作,从而实现主从操作一致,最终数据一致。
2 一主多从配置
服务器规划:使用docker方式创建,主从服务器IP一致,端口号不一致 三个服务器都有相同的数据库db_user
- 主服务器:容器名
fy-mysql-master,端口3309 - 从服务器:容器名
fy-mysql-slave1,端口3307 - 从服务器:容器名
fy-mysql-slave2,端口3308
注意: 如果此时防火墙是开启的,则先关闭防火墙,并重启docker,否则后续安装的MySQL无法启动
1 | |
2.1 准备主服务器(已完成)
step1:在docker中创建并启动MySQL主服务器:
端口3309就用之前搭建好的fy-mysql8
step2:创建MySQL主服务器配置文件:
默认情况下MySQL的binlog日志是自动开启的,可以通过如下配置定义一些可选配置
1 | |
配置如下内容
1 | |
重启MySQL容器
1 | |
binlog格式说明:
binlog_format=STATEMENT:日志记录的是主机数据库的
写指令,性能高,但是now()之类的函数以及获取系统参数的操作会出现主从数据不同步的问题。binlog_format=ROW(默认):日志记录的是主机数据库的
写后的数据,批量操作时性能较差,解决now()或者 user()或者 @@hostname 等操作在主从机器上不一致的问题。binlog_format=MIXED:是以上两种level的混合使用,有函数用ROW,没函数用STATEMENT,但是无法识别系统变量
step3:主机中查询master状态:
执行完此步骤后不要再操作主服务器MYSQL,防止主服务器状态值变化
1 | |
记下File和Position的值。执行完此步骤后不要再操作主服务器MYSQL,防止主服务器状态值变化。
2.2 准备从服务器
可以配置多台从机slave1、slave2…,这里以配置slave1为例
- step1:在docker中创建并启动MySQL从服务器:
端口3307
1 | |
- step2:创建MySQL从服务器配置文件:
1 | |
配置如下内容:
1 | |
重启MySQL容器
1 | |
- step3:在从机上配置主从关系:
在从机上执行以下SQL操作
1 | |
2.3 启动主从同步
启动从机的复制功能,执行SQL:
1 | |
两个关键进程: 下面两个参数都是Yes,则说明主从配置成功!
2.4 停止和重置
需要的时候,可以使用如下SQL语句
1 | |
2.5 常见问题
问题1
启动主从同步后,常见错误是Slave_IO_Running: No 或者 Connecting 的情况,此时查看下方的 Last_IO_ERROR错误日志,根据日志中显示
的错误信息在网上搜索解决方案即可
典型的错误例如:Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Client requested master to start replication from position > file size'
解决方案:
1 | |
问题2
启动docker容器后提示 WARNING: IPv4 forwarding is disabled. Networking will not work.
此错误,虽然不影响主从同步的搭建,但是如果想从远程客户端通过以下方式连接docker中的MySQL则没法连接
1 | |
解决方案:
1 | |
2.6 测试实现主从同步
在主机中执行以下SQL,在从机中查看数据库、表和数据是否已经被同步
1 | |
2.1 配置文件内容
1 | |
为了防止订单号重复 需要先 修改Order实体类的主键策略:
1 | |
测试:保留上面配置中的一个分片表节点分别进行测试,检查每个分片节点是否可用
1 | |
2.3 行表达式
优化上一步的分片表配置
https://shardingsphere.apache.org/document/5.1.1/cn/features/sharding/concept/inline-expression/
1 | |
2.4 分布式序列算法
雪花算法:
https://shardingsphere.apache.org/document/5.1.1/cn/features/sharding/concept/key-generator/
水平分片需要关注全局序列,因为不能简单的使用基于数据库的主键自增。
分库分表以后表的主键字段就不能使用自增策略,因为可能出现重复数据。
分布式系统中的主键生成策略:
1、uuid
2、redis
3、雪花算法(shardingsphere:SnowflakeKeyGenerateAlgorithm)
雪花算法是推特开源的分布式ID生成算法,用于在不同的机器上生成唯一的ID的算法。该算法生成一个64bit的数字作为分布式ID,保证这个ID自增
并且全局唯一。
生成的64位ID结构如下:
雪花算法是 64 位 的二进制,一共包含了四部分:
1、1位是符号位,也就是最高位,始终是0,没有任何意义,因为要是唯一计算机二进制补码中就是负数,0才是正数。
2、41位是时间戳,具体到毫秒,41位的二进制可以使用69年,如果时间计算基准年为1970年,那么到2039年就不能再用了,当前也可以指定一个基准年。
3、10位是机器标识,可以全部用作机器ID,也可以用来标识机房ID + 机器ID,10位最多可以表示1024台机器。
4、12位是计数序列号,也就是同一台机器上同一时间,理论上还可以同时生成不同的ID,12位的序列号能够区分出4096个ID。
这里有两种方案:一种是基于MyBatisPlus的id策略;一种是ShardingSphere-JDBC的全局序列配置。
基于MyBatisPlus的id策略:将Order类的id设置成如下形式: 以上演示就是基于此种方式
1 | |
基于ShardingSphere-JDBC的全局序列配置:和前面的MyBatisPlus的策略二选一
我们在下一个多表关联案例中演示即可
1 | |
此时,需要将实体类中的id策略修改成以下形式:
1 | |
[如何避免主从延迟?]
读写分离对于提升数据库的并发非常有效,但是,同时也会引来一个问题:主库和从库的数据存在延迟,比如你写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 主从同步延迟 。
如果我们的业务场景无法容忍主从同步延迟的话,应该如何避免呢(注意:我这里说的是避免而不是减少延迟)?
这里提供两种我知道的方案(能力有限,欢迎补充),你可以根据自己的业务场景参考一下。
[强制将读请求路由到主库处理]
既然你从库的数据过期了,那我就直接从主库读取嘛!这种方案虽然会增加主库的压力,但是,实现起来比较简单,也是我了解到的使用最多的一种方式。
比如 Sharding-JDBC 就是采用的这种方案。通过使用 Sharding-JDBC 的 HintManager 分片键值管理器,我们可以强制使用主库。
1 | |
对于这种方案,你可以将那些必须获取最新数据的读请求都交给主库处理。
[延迟读取]
还有一些朋友肯定会想既然主从同步存在延迟,那我就在延迟之后读取啊,比如主从同步延迟 0.5s,那我就 1s 之后再读取数据。这样多方便啊!方便是方便,但是也很扯淡。
不过,如果你是这样设计业务流程就会好很多:对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作。比如你支付成功之后,跳转到一个支付成功的页面,当你点击返回之后才返回自己的账户。
[总结]
关于如何避免主从延迟,我们这里介绍了两种方案。实际上,延迟读取这种方案没办法完全避免主从延迟,只能说可以减少出现延迟的概率而已,实际项目中一般不会使用。
总的来说,要想不出现延迟问题,一般还是要强制将那些必须获取最新数据的读请求都交给主库处理。如果你的项目的大部分业务场景对数据准确性要求不是那么高的话,这种方案还是可以选择的。
[什么情况下会出现主从延迟?如何尽量减少延迟?]
MySQL 主从同步延时是指从库的数据落后于主库的数据,这种情况可能由以下两个原因造成:
- 从库 I/O 线程接收 binlog 的速度跟不上主库写入 binlog 的速度,导致从库 relay log 的数据滞后于主库 binlog 的数据;
- 从库 SQL 线程执行 relay log 的速度跟不上从库 I/O 线程接收 binlog 的速度,导致从库的数据滞后于从库 relay log 的数据。
那什么情况下会出现出从延迟呢?这里列举几种常见的情况:
- 从库机器性能比主库差:从库接收 binlog 并写入 relay log 以及执行 SQL 语句的速度会比较慢(也就是 T2-T1 和 T3-T2 的值会较大),进而导致延迟。解决方法是选择与主库一样规格或更高规格的机器作为从库,或者对从库进行性能优化,比如调整参数、增加缓存、使用 SSD 等。
- 从库处理的读请求过多:从库需要执行主库的所有写操作,同时还要响应读请求,如果读请求过多,会占用从库的 CPU、内存、网络等资源,影响从库的复制效率(也就是 T2-T1 和 T3-T2 的值会较大,和前一种情况类似)。解决方法是引入缓存(推荐)、使用一主多从的架构,将读请求分散到不同的从库,或者使用其他系统来提供查询的能力,比如将 binlog 接入到 Hadoop、Elasticsearch 等系统中。
- 大事务:运行时间比较长,长时间未提交的事务就可以称为大事务。由于大事务执行时间长,并且从库上的大事务会比主库上的大事务花费更多的时间和资源,因此非常容易造成主从延迟。解决办法是避免大批量修改数据,尽量分批进行。类似的情况还有执行时间较长的慢 SQL ,实际项目遇到慢 SQL 应该进行优化。
- 网络延迟:如果主从之间的网络传输速度慢,或者出现丢包、抖动等问题,那么就会影响 binlog 的传输效率,导致从库延迟。解决方法是优化网络环境,比如提升带宽、降低延迟、增加稳定性等。
- 单线程复制:MySQL5.5 及之前,只支持单线程复制。为了优化复制性能,MySQL 5.6 引入了 多线程复制,MySQL 5.7 还进一步完善了多线程复制。
- 复制模式:MySQL 默认的复制是异步的,必然会存在延迟问题。全同步复制不存在延迟问题,但性能太差了。半同步复制是一种折中方案,相对于异步复制,半同步复制提高了数据的安全性,减少了主从延迟(还是有一定程度的延迟)。MySQL 5.5 开始,MySQL 以插件的形式支持 semi-sync 半同步复制。并且,MySQL 5.7 引入了 增强半同步复制 。
常见的分片算法有哪些
分片算法主要解决了数据被水平分片之后,数据究竟该存放在哪个表的问题。
常见的分片算法有:
- 哈希分片:求指定分片键的哈希,然后根据哈希值确定数据应被放置在哪个表中。哈希分片比较适合随机读写的场景,不太适合经常需要范围查询的场景。哈希分片可以使每个表的数据分布相对均匀,但对动态伸缩(例如新增一个表或者库)不友好。
- 范围分片:按照特定的范围区间(比如时间区间、ID 区间)来分配数据,比如 将
id为1~299999的记录分到第一个表,300000~599999的分到第二个表。范围分片适合需要经常进行范围查找且数据分布均匀的场景,不太适合随机读写的场景(数据未被分散,容易出现热点数据的问题)。 - 映射表分片:使用一个单独的表(称为映射表)来存储分片键和分片位置的对应关系。映射表分片策略可以支持任何类型的分片算法,如哈希分片、范围分片等。映射表分片策略是可以灵活地调整分片规则,不需要修改应用程序代码或重新分布数据。不过,这种方式需要维护额外的表,还增加了查询的开销和复杂度。
- 一致性哈希分片:将哈希空间组织成一个环形结构,将分片键和节点(数据库或表)都映射到这个环上,然后根据顺时针的规则确定数据或请求应该分配到哪个节点上,解决了传统哈希对动态伸缩不友好的问题。
- 地理位置分片:很多 NewSQL 数据库都支持地理位置分片算法,也就是根据地理位置(如城市、地域)来分配数据。
- 融合算法分片:灵活组合多种分片算法,比如将哈希分片和范围分片组合。
分库分表会带来什么问题呢)、
记住,你在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求,是否适合当前业务场景,还要重点考虑其带来的成本。
引入分库分表之后,会给系统带来什么挑战呢?
- join 操作:同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。不过,很多大厂的资深 DBA 都是建议尽量不要使用 join 操作。因为 join 的效率低,并且会对分库分表造成影响。对于需要用到 join 操作的地方,可以采用多次查询业务层进行数据组装的方法。不过,这种方法需要考虑业务上多次查询的事务性的容忍度。
- 事务问题:同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。这个时候,我们就需要引入分布式事务了。关于分布式事务常见解决方案总结,网站上也有对应的总结:https://javaguide.cn/distributed-system/distributed-transaction.html 。
- 分布式 ID:分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 ID 了。关于分布式 ID 的详细介绍&实现方案总结,可以看我写的这篇文章:分布式 ID 介绍&实现方案总结。
- 跨库聚合查询问题:分库分表会导致常规聚合查询操作,如 group by,order by 等变得异常复杂。这是因为这些操作需要在多个分片上进行数据汇总和排序,而不是在单个数据库上进行。为了实现这些操作,需要编写复杂的业务代码,或者使用中间件来协调分片间的通信和数据传输。这样会增加开发和维护的成本,以及影响查询的性能和可扩展性。
- ……
另外,引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本。
[什么是数据冷热分离?]
数据冷热分离是指根据数据的访问频率和业务重要性,将数据分为冷数据和热数据,冷数据一般存储在存储在低成本、低性能的介质中,热数据高性能存储介质中。
冷数据和热数据
热数据是指经常被访问和修改且需要快速访问的数据,冷数据是指不经常访问,对当前项目价值较低,但需要长期保存的数据。
冷热数据到底如何区分呢?有两个常见的区分方法:
- 时间维度区分:按照数据的创建时间、更新时间、过期时间等,将一定时间段内的数据视为热数据,超过该时间段的数据视为冷数据。这种方法适用于数据的访问频率和时间有较强的相关性的场景。
- 访问频率区分:将高频访问的数据视为热数据,低频访问的数据视为冷数据。适合访问频率和数据本身有较强的相关性的场景。
几年前的数据并不一定都是冷数据,例如一些优质文章发表几年后依然有很多人访问,大部分普通用户新发表的文章却基本没什么人访问。这两种区分冷热数据的方法各有优劣,实际项目中,可以将两者结合使用。
数据冷热分离的优缺点
- 优点:热数据的查询性能得到优化(用户的绝大部分操作体验会更好)、节约成本(可以冷热数据的不同存储需求,选择对应的数据库类型和硬件配置,比如将热数据放在 SSD 上,将冷数据放在 HDD 上)
- 缺点:系统复杂性和风险增加(需要分离冷热数据,数据错误的风险增加)、统计效率低(统计的时候可能需要用到冷库的数据)。
冷数据如何迁移?
冷数据迁移方案:
- 业务层代码实现:当有对数据进行写操作时,触发冷热分离的逻辑,判断数据是冷数据还是热数据,冷数据就入冷库,热数据就入热库。这种方案会影响性能且冷热数据的判断逻辑不太好确定,还需要修改业务层代码,因此一般不会使用。
- 任务调度:可以利用 xxl-job 或者其他分布式任务调度平台定时去扫描数据库,找出满足冷数据条件的数据,然后批量地将其复制到冷库中,并从热库中删除。这种方法修改的代码非常少,非常适合按照时间区分冷热数据的场景。
- 监听数据库的变更日志 binlog :将满足冷数据条件的数据从 binlog 中提取出来,然后复制到冷库中,并从热库中删除。这种方法可以不用修改代码,但不适合按照时间维度区分冷热数据的场景。
如果你的公司有 DBA 的话,也可以让 DBA 进行冷数据的人工迁移,一次迁移完成冷数据到冷库。然后,再搭配上面介绍的方案实现后续冷数据的迁移工作。
冷数据如何存储?
冷数据的存储要求主要是容量大,成本低,可靠性高,访问速度可以适当牺牲。
冷数据存储方案:
- 中小厂:直接使用 MySQL/PostgreSQL 即可(不改变数据库选型和项目当前使用的数据库保持一致),比如新增一张表来存储某个业务的冷数据或者使用单独的冷库来存放冷数据(涉及跨库查询,增加了系统复杂性和维护难度)
- 大厂:Hbase(常用)、RocksDB、Doris、Cassandra
如果公司成本预算足的话,也可以直接上 TiDB 这种分布式关系型数据库,直接一步到位。

end