数据库分库分表

一个初创时运行良好的单体数据库,往往在用户量和业务复杂度攀升后,面临响应缓慢、连接耗尽、甚至服务不可用的窘境。传统的垂直扩展(Scale Up)方案是升级服务器配置(更强的CPU、更大的内存、更快的SSD),这种方案成本高昂且存在物理极限。因此,水平扩展(Scale Out) 成为必然选择,即通过增加机器数量来分散负载。而读写分离、分库分表,正是实现水平扩展的具体手段。

读写分离

读写分离是最基础、最常用的数据库优化手段。其核心思想是将数据库的读操作和写操作路由到不同的数据库实例上执行。主库负责处理所有的写操作(增删改),是数据的唯一源头。从库通过主从复制技术,实时或准实时地同步主库的数据;从库只负责处理读操作(查询)。这样可以有效提升数据库读性能。

核心技术点:

  • 主从复制:MySQL主从复制的工作流程如下:当Master节点接收到用户的写请求后,会将操作记录写入本地的二进制日志(binary log)。Slave节点通过其I/O线程与Master建立连接,并发送"binlog dump"命令请求日志数据。Master接收到请求后,会将binary log中的数据推送给Slave。Slave将接收到的数据写入自身的中继日志(relay log)。随后,Slave的SQL线程读取并执行relay log中的操作,从而将数据变更同步到本地数据库,完成数据复制。(详见:《MySQL主从复制》)
  • 主从复制的延迟问题处理 :MySQL主从复制存在固有的同步延迟,通常为1秒左右,大批量数据同步时可能长达1分钟。由于数据通过网络从主库(Master)异步复制到从库(Slave),在写入主库后立即从从库读取,可能因数据尚未同步而无法获取最新数据,导致业务异常。例如,用户注册后立即登录,系统可能提示"未注册",尽管注册已成功。对于强一致性要求的场景(如插入后需立即读取 last_insert_id),必须确保读操作能访问最新数据。常见解决方案
    • 1、将写操作后的读请求强制路由至主库(通过 hint 或 API 指定),但该方式与业务逻辑耦合度高、侵入性强。
    • 2、先尝试从从库读取,失败后再从主库读取(二次读取),此方法对业务透明、实现简单,但频繁回切会增加主库负载。
    • 3、将关键业务的读写操作全部指向主库,非关键业务采用读写分离,可以兼顾一致性与系统性能。
  • 配置中心
    • 在数据库主从复制架构中,为了实现高可用性(HA)与可扩展性(Scalable),通常需要引入配置中心来支持自动处理主从切换、从库的扩容与缩容(比如新增加的从节点也能被及时识别,并将读请求分发至新节点)、故障转移(比如某个从节点失效,系统会将读请求重新路由到其他正常运行的从节点)等操作。对于数据库中间件(无论是 proxy 还是 smart-client 而言),这些调整仅涉及配置信息的更新。因此,所有配置变更均记录于配置中心,包括主从切换时的新主从信息以及新增从库的IP和端口等细节。数据库中间件通过监听这些配置变更,确保实时应用最新配置,从而保障系统的平稳运行。
  • 流量路由方式:将读写操作区分开来,然后访问不同的数据库服务器。
    • 1、程序代码封装方式(smart-client 模式) :在代码层面抽象出一个数据访问层来管理读写操作分离和数据库服务器的连接。这种方式实现相对简单且可根据业务需求进行定制化调整,但每种编程语言都需要独立开发,对于多语言编写的系统而言,增加了重复开发的工作量,并且在主从切换时需要对所有系统进行配置更新和重启。开源项目如淘宝的 TDDL 即采用此方法,基于集中式配置实现 JDBC 数据源,支持主备切换、读写分离等功能。
    • 2、中间件封装方式(proxy 模式) :通过独立的系统提供标准 SQL 接口来实现读写分离,这种方式能够支持多种编程语言,但由于需处理完整的 SQL 语法和数据库协议,其实现更为复杂,要求较高的性能,不过它能自动感知数据库主从状态并进行相应调整,无需业务服务器做出改动。开源项目如 MySQL Router,它能够在主从切换时保持业务逻辑的透明性。
  • 应用开发要点
    • 基本读写分离功能
      • 程序代码封装主从库路由的方式下,核心思想是将写操作(如 INSERT、UPDATE、DELETE) 发送到主库(Master),而将读操作(如 SELECT) 分发到一个或多个从库(Slave)。因此,需要在应用程序中配置两套数据库连接:主库连接用于执行写操作;从库连接用于执行读操作。
      • 中间件封装主从库路由的方式下,读写分离中间件内部的实现一般默认就是写操作走主库,读操作走从库。同时,某些中间件(如 MyCat、ShardingSphere、阿里巴巴的 TDDL)支持通过 SQL Hint 来强制指定某条读 SQL 走主库,以解决主从延迟导致的数据不一致问题。
    • 事务问题:在数据库读写分离架构下,事务中的所有操作通常必须路由到主库执行,以保证数据的一致性。因为事务可能包含写操作,且后续读操作需要看到之前写入的最新数据,若将读请求分发到有延迟的从库,会导致数据不一致。因此,在事务开始后,所有SQL(包括读)都应强制走主库,直到事务提交或回滚后,读请求才可恢复由从库处理。
    • 感知集群信息变更:在数据库读写分离架构中,通过配置中心集中管理主从数据库的地址、状态和权重等集群信息,应用程序启动时从配置中心获取连接信息,并监听其变更;当数据库集群发生主从切换、新增从库或节点故障时,运维人员或高可用组件将更新后的配置写入配置中心,应用实例收到通知后动态刷新本地缓存的集群信息,从而无需重启即可自动感知拓扑变化并调整SQL路由策略,实现平滑的故障转移与弹性伸缩。

分库分表

当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面:

  • 数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
  • 数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
  • 数据文件越大,极端情况下丢失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)。

这时候就需要通过分库分表来分散存储压力,同时还能分散访问压力。

业务分库

业务分库就是按照不同的业务模块或功能领域,将原本集中在一个数据库中的数据拆分到多个独立的数据库中。例如,将用户服务、订单服务、商品服务的数据分别存储在 user_dborder_dbproduct_db 中。每个库独立部署,拥有自己的表结构和数据,服务之间通过接口通信,实现数据库层面的物理隔离。

引入问题:

  • 跨库事务复杂:跨库操作无法使用本地事务保证一致性,需要使用分布式事务管理(如Seata、XA协议)、柔性事务(最终一致性)等方案保证数据一致性,增加系统复杂度。
  • 跨库查询困难:无法直接通过 SQL 联表查询不同库的数据,需在应用层聚合或借助中间件、数据同步到数据仓库等方案解决。
  • 维护成本上升:多个数据库实例需要独立监控、备份、扩容,运维复杂度增加。
  • 初期拆分需谨慎:拆分过细导致系统碎片化,过粗则无法有效缓解压力,且后期合并或调整成本高。

应用场景:

  • 大型系统微服务化:微服务架构下,每个服务拥有独立数据库,天然实现业务分库,如电商系统中的用户、订单、库存等服务。
  • 高并发、大数据量的独立业务模块:某个业务(如订单)数据增长快、访问量大,独立成库可单独优化、扩容,避免影响其他业务。
  • 安全与权限隔离需求:敏感业务(如支付)需独立数据库,便于实施更严格的访问控制和审计策略。

分表

当同一业务的单表数据达到单台数据库服务器的处理瓶颈(比如单表超过500w行,或单表容量超过2GB),此时就需要对单表数据进行拆分。

MySQL单表最大记录数不能超过多少呢?

MySQL单表最大记录数没有固定上限,主要受配置和硬件条件影响。由于MySQL(尤其是InnoDB引擎)会将索引加载到内存以提升性能,当 InnoDB buffer size 足够时,索引可完全驻留内存,查询效率较高;但当单表数据量达到一定规模,索引无法全部装入内存,后续查询将引发磁盘IO,导致性能下降。当然,这个还和具体的表结构的设计有关,对于一些比较复杂的表,可能超过1000万就要分表了;而对于一些简单的表,即使存储数据超过1亿行,也可以不分表。最终导致性能问题的主要都是内存限制。

分表方式包括垂直拆分和水平拆分两种:

垂直分表

垂直拆分是指将一个数据库中的表按照列(字段)或表本身,拆分到不同的数据库实例中。

两种主要的垂直拆分形式:

  • 垂直分库:将不同业务模块的表拆分到不同的数据库实例中,也常被称为"按业务分库"。适合将表中某些不常用且占了大量空间的列拆分出去。
    • 例如:将 user_db(用户相关表:users, profiles)和 order_db(订单相关表:orders, order_items)拆分成两个独立的数据库实例。
  • 垂直分表:将一张大表的列(字段)根据访问频率、数据类型或业务逻辑,拆分到多个不同的表中,这些表可以存放在同一个数据库,也可以存放在不同的数据库实例中。一个典型应用场景是服务化(SOA)改造:服务化改造除了业务上需要进行拆分,底层的存储也需要进行隔离。
    • 例如:将 users 表拆分为 user_basic(ID, name, email)和 user_profile(ID, avatar, address)。

水平分表

当单个业务模块的数据量也达到瓶颈时(如订单表已超亿级),垂直拆分也无法解决。这时就需要水平拆分。

水平拆分是将同一张表的数据,按照某种规则(分片键,Sharding Key)水平切分到多个数据库或多个表中。每个分片只存储一部分数据。

通过水平拆分,可以突破单表容量瓶颈,理论上通过增加分片数量,可以无限扩展数据存储能力。

核心技术点:

  • 全局唯一ID:需要统一的ID生成策略(如雪花算法Snowflake)来保证不同库之间的ID不冲突。详见《分布式 ID》。
  • 分片方式
    • 仅分库:每个分片是一个独立的数据库。
    • 仅分表 :所有分片在同一数据库的不同表中(如orders_0, orders_1)。
    • 分库又分表:最常见的组合,兼顾了数据库和表的负载。
  • 分片策略(路由策略):
    • 分片键:用于决定数据路由的字段,选择正确的分片键至关重要。
      • 分片键应使得数据尽可能平均拆分,能够让所有存储节点均衡地负载读写请求,避免热点问题。
      • 分片键应是查询条件中最常使用的字段,确保大多数查询能精准定位到单个分片。应尽量每条 SQL 都尽可能带上分片键,避免全表扫描。
      • 一般使用业务主键做路由,这样相同业务的不同表可以落在同一个库中,方便 join 等联表操作。子业务表也要冗余与主业务表相同的分片键。
      • 常见分片键有:用户ID(适用于以用户为中心的业务)、订单ID、地理区域(如城市ID)、时间(如按月份分表)。
    • 分片算法
      • 取模 :根据分片键(如ID)取模决定分片,比如shard_id = user_id % 4。数据分布均匀,简单高效,但扩容困难。
      • 范围分片:按数值或时间范围分片(如0-100万,100-200万),对范围查询友好,但数据可能不均(容易导致热点问题)。
      • 哈希分片:先对分片键哈希,再取模,分布更均匀,但对范围查询不友好。
      • 一致性哈希分片:节点增减时,数据迁移量最小,适合动态扩容。
  • 流量路由方式
    • 客户端代理方式(smart-client 模式) :在应用程序中集成分片逻辑的 SDK 或驱动(如 ShardingSphere-JDBC),由客户端直接根据配置的分片规则解析 SQL、计算路由并连接正确的数据库实例执行操作。该方式无需额外部署代理服务,性能较高、延迟低,但会增加应用的依赖和复杂性,且需为不同语言维护各自的客户端组件。
    • 服务端代理方式(proxy 模式) :在数据库前部署一个独立的代理服务(如 MyCat、ShardingSphere-Proxy、MaxScale),应用程序像连接单库一样连接代理。代理负责解析 SQL、提取分片键、计算目标分片,并将请求转发到后端对应的数据库实例,再将结果返回给应用。该方式对应用透明,支持多种语言,便于集中管理,但多一层网络转发,可能成为性能瓶颈或单点故障。
  • 分库分表中间件
    • 分库分表中间件通过解析 SQL 语句,提取分片键(Sharding Key)并根据预设的分片算法(如哈希、取模、范围)计算目标数据节点,对于写操作(INSERT/UPDATE/DELETE),将 SQL 路由到对应的一个或多个分库分表中执行;对于读操作(SELECT),若查询带分片键则精准路由到单个分片,若为广播查询或无分片键,则可能路由到多个分片并聚合结果,最终由中间件统一返回数据,从而对应用屏蔽底层数据分布的复杂性。
  • 常见的分库分表中间件
中间件 类型 核心特点 支持语言 优点 缺点
ShardingSphere (Apache) 客户端 + 服务端 开源生态完善,支持分库分表、读写分离、分布式事务、数据加密等;提供 JDBC(客户端)和 Proxy(服务端)两种模式。 Java(JDBC),多语言(Proxy) 功能全面,社区活跃,可灵活选择部署模式,对应用透明度高。 配置较复杂,学习成本较高;大规模集群下Proxy有性能瓶颈。
MyCat / MyCat2 服务端代理 基于 MySQL 协议的代理层,模拟 MySQL 服务器,支持分库分表、读写分离。 多语言(通过 MySQL 协议) 使用简单,兼容 MySQL 协议,对应用透明。 社区活跃度一般,功能扩展性较弱,性能受限于代理层,存在单点风险。
Vitess 服务端代理 由 YouTube 开发并开源,专为 MySQL 设计的大规模数据库集群管理平台,支持自动分片、弹性扩容、拓扑管理。 多语言(gRPC/MySQL 协议) 成熟稳定,适合超大规模场景,自动化程度高,被 YouTube 和 GitHub 等验证。 架构复杂,部署和运维门槛高,主要面向 Kubernetes 环境。
TDDL (阿里) 客户端 阿里早期自研的分库分表框架,集成在应用中,基于 Spring 和 JDBC 扩展。 Java 性能高,与阿里技术栈深度集成。 未完全开源,社区支持弱,仅适合 Java,维护成本高。
Cobar (阿里) 服务端代理 阿里早期开源的数据库中间件,后被 MyCat 基于其思想发展。 多语言 历史悠久,启发了后续中间件发展。 已停止维护,功能陈旧,不推荐新项目使用。
ProxySQL 服务端代理 高性能 MySQL 代理,主要用于读写分离和查询缓存,可通过规则实现简单分片。 多语言 性能极高,轻量稳定,支持查询缓存和负载均衡。 分库分表功能较弱,需配合外部逻辑实现复杂分片。

分库分表引入的问题:

  • 跨表 JOIN 困难:水平分表后,数据分散在多个表中,如果需要与其他表进行 join 查询,需要在业务代码或者数据库中间件中进行多次 join 查询,然后将结果合并,性能开销大。或者使用性能更优但实现更复杂的方式,比如小表广播 join 。
    • 小表广播 join 将一个数据量小的表(小表)完整复制并广播到所有计算节点上,使得每个节点都持有该小表的全量数据,然后与本地存储的大表分片进行本地Join操作,从而避免跨节点的数据传输和网络通信开销,显著提升查询性能。主要适用于小表与大表关联的场景,如维度表与事实表的 join,常见于 Hive、Spark、Flink 等分布式计算框架中。
  • 跨分片查询难COUNT(*)GROUP BYORDER BY、分页等操作涉及多个分片,需在应用层汇总。 如果对这些跨分片查询需求频繁的话,可考虑引入搜索引擎来处理这些复杂查询。
  • 分布式事务: 跨库操作无法使用本地事务保证一致性,需要使用分布式事务管理(如Seata、XA协议)、柔性事务(最终一致性)等方案保证数据一致性。详见:《分布式事务与最终一致性》。

(【本文首发于:JavaArchJourney】)

相关推荐
ZhangBlossom2 小时前
【Java】EasyExcel实现导入导出数据库中的数据为Excel
java·数据库·excel
不见长安在3 小时前
redis集群下如何使用lua脚本
数据库·redis·lua
可观测性用观测云3 小时前
阿里云 RDS PostgreSQL 可观测最佳实践
数据库
馨谙3 小时前
SELinux 文件上下文管理详解:从基础到实战
jvm·数据库·oracle
ClouGence3 小时前
百草味数据架构升级实践:打造 Always Ready 的企业级数据平台
大数据·数据库·数据分析
熙客3 小时前
Kafka:专注高吞吐与实时流处理的分布式消息队列
分布式·中间件·kafka
川石课堂软件测试3 小时前
Python | 高阶函数基本应用及Decorator装饰器
android·开发语言·数据库·python·功能测试·mysql·单元测试
.又是新的一天.4 小时前
08-Jmeter数据驱动、数据库的操作、命令行执行方式
数据库·jmeter
LilySesy4 小时前
ABAP+如果在join的时候需要表1的字段某几位等于表2的字段的某几位,需要怎么做?
服务器·前端·数据库·sap·abap·alv