首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >分库分表实战手册:从 0 到 1 拆解亿级数据存储难题,Java 工程师避坑指南

分库分表实战手册:从 0 到 1 拆解亿级数据存储难题,Java 工程师避坑指南

作者头像
果酱带你啃java
发布2026-04-14 11:06:20
发布2026-04-14 11:06:20
490
举报

当你的业务从日活百万跃升到千万,数据库单表数据量突破 5000 万行时,是否遇到过这些噩梦:简单查询耗时从毫秒级飙升到秒级,索引优化杯水车薪,DDL 操作导致全表锁死,服务器 IO 被频繁的磁盘扫描占满?

我深耕 Java 后端十余年,亲历过三次从单库单表到分库分表的架构迁移,见证过因拆分策略失误导致的 "二次重构",也踩过分布式事务、跨表查询的各种深坑。本文将系统拆解分库分表的底层逻辑,结合 ShardingSphere 实战案例,手把手教你实现从 "卡成 PPT" 到 "亿级数据秒查" 的蜕变,附带 10 + 生产级代码示例和 20 + 避坑指南。

一、为什么分库分表是 Java 工程师的必修课?

在互联网业务爆发式增长的今天,"数据量" 和 "访问量" 的双重爆炸正在瓦解传统单库单表的根基。理解分库分表的必要性,首先要认清单库单表的三大死穴。

1.1 单库单表的极限在哪里?

MySQL 作为最主流的关系型数据库,在单库单表场景下存在明确的性能临界点:

指标

安全阈值

危险阈值

崩溃阈值

单表数据量

<1000 万行

1000 万 - 5000 万行

>5000 万行

日均新增数据

<100 万行

100 万 - 500 万行

>500 万行

单表索引数量

<5 个

5-10 个

>10 个

每秒查询量 (QPS)

<1000

1000-5000

>5000

超过安全阈值后,会出现一系列难以解决的问题:

  • 查询性能断崖式下降B + 树索引高度超过 4 层,磁盘 IO 次数激增(从 3-4 次增至 8-10 次)
  • 写入阻塞大量并发写入导致行锁、表锁竞争加剧,事务提交耗时增加
  • 维护成本飙升添加索引、表结构变更需要数小时,甚至导致服务不可用
  • 资源瓶颈凸显单库 CPU、内存、IO 使用率常年超过 80%,扩容无门

某电商平台真实案例:订单表数据量达 8000 万行后,"查询用户近 3 个月订单" 接口响应时间从 300ms 增至 5s+,每日因超时导致的订单投诉增长 300%,数据库服务器 IO 利用率持续 95% 以上,最终被迫紧急停机分表。

1.2 分库分表的核心价值

分库分表(Sharding)通过 "拆分" 打破单库单表的物理限制,本质是一种 "分而治之" 的水平扩展策略:

  • 突破硬件瓶颈将数据分散到多台服务器,解决单服务器的 CPU、内存、IO 上限
  • 提升并发能力多库多表并行处理请求,总吞吐量(TPS/QPS)随节点增加线性增长
  • 降低维护成本小表的索引重建、数据备份、DDL 操作耗时大幅减少(从小时级降至分钟级)
  • 优化资源利用率按业务冷热分离数据,热点数据部署到高性能服务器,冷数据用低成本存储

经过验证的性能收益:某支付系统分库分表后,单表数据量从 6000 万降至 500 万,查询响应时间降低 92%,写入 TPS 提升 7 倍,年服务器成本降低 40%(用 10 台普通服务器替代 2 台小型机)。

1.3 分库分表的三大误区

很多团队在分库分表时存在认知偏差,导致从一开始就埋下隐患:

  1. "数据量大了再分":等到性能崩溃时再拆分,数据迁移难度呈指数级增长,可能需要停机数小时
  2. "分的越细越好":过度拆分导致跨表查询复杂,事务一致性难以保证,运维成本激增
  3. "照搬别人的方案":盲目套用电商的订单分表策略到社交场景,忽略业务特性导致性能不升反降

正确的做法是:提前规划,按需拆分,因业务制宜。在数据量达到安全阈值的 70% 时就启动分库分表设计,根据业务访问模式选择最合适的拆分策略。

二、分库分表核心概念:从 "拆什么" 到 "怎么拆"

分库分表不是简单的 "一拆了之",需要先理解其底层逻辑和核心术语,避免在实战中 "知其然不知其所以然"。

2.1 垂直拆分 vs 水平拆分

分库分表的两种基本拆分方式,适用场景截然不同:

垂直拆分:按业务领域切割

核心思想:将单一数据库或表按业务功能拆分到不同的库或表,实现 "专库专表"。

垂直分库:按业务模块拆分数据库(如用户库、订单库、商品库)

原单库:mall_db

代码语言:javascript
复制
拆分后:
- user_db(用户相关表)
- order_db(订单相关表)
- product_db(商品相关表)

垂直分表:将大表按字段冷热拆分(如将用户表拆分为基本信息表和详情表)

-- 原大表

代码语言:javascript
复制
CREATETABLE`user`(
`id`bigintNOTNULL,
`username`varchar(50)NOTNULL,-- 高频访问
`password`varchar(100)NOTNULL,-- 高频访问
`avatar`varchar(255)DEFAULTNULL,-- 低频访问
`introduction`textDEFAULTNULL,-- 低频访问,大字段
...
);

-- 垂直分表后
CREATETABLE`user_base`(-- 高频访问,小字段
`id`bigintNOTNULL,
`username`varchar(50)NOTNULL,
`password`varchar(100)NOTNULL,
...
);

CREATETABLE`user_profile`(-- 低频访问,大字段
`user_id`bigintNOTNULL,
`avatar`varchar(255)DEFAULTNULL,
`introduction`textDEFAULTNULL,
...
);

适用场景

  • 单表字段过多(>50 个),大字段(text、blob)占比高
  • 不同字段访问频率差异大(如 90% 查询只用到 10% 的字段)
  • 数据库负载集中在某几个核心表,其他表资源空闲

优点

  • 拆分规则简单,按业务边界划分
  • 便于专人维护不同业务的库表
  • 可针对不同业务特性优化存储(如商品表用 SSD,日志表用普通硬盘)

缺点

  • 无法解决单表数据量过大的问题(仍可能存在千万级大表)
  • 跨业务查询需联表,复杂度增加(如查询 "用户下单商品" 需跨库联表)
水平拆分:按数据行切割

核心思想:将大表的行数据按规则分散到多个结构相同的表或库中,实现 "数据分片"。

  • 水平分表:同库内拆分(如订单表拆分为 order_0 到 order_31)
  • 水平分库:跨库拆分(如 order_db_0 到 order_db_3,每个库包含 order_0 到 order_7)

适用场景

  • 单表数据量超过安全阈值(>1000 万行)
  • 读写压力集中在某张表,垂直拆分无法缓解
  • 数据有明显的分片维度(如按用户 ID、时间范围)

优点

  • 彻底解决单表数据量过大的问题
  • 可无限扩展(理论上),通过增加节点提升性能
  • 分片表结构相同,业务代码改动小

缺点

  • 拆分规则复杂,需精准设计
  • 跨分片查询难度大(如统计全量数据)
  • 分布式事务难以保证

2.2 分库分表的关键术语

理解这些术语是掌握分库分表工具的基础:

  • 分片键(Sharding Key)用于拆分数据的字段(如 user_id、order_time),是分库分表的 "灵魂"
  • 分片规则(Sharding Rule)定义如何根据分片键拆分数据(如哈希取模、范围划分)
  • 分片(Shard)拆分后的每个数据片段(可以是表或库)
  • 逻辑表(Logical Table)拆分前的表名(如 order),用于统一访问入口
  • 真实表(Actual Table)拆分后的实际表(如 order_0、order_1)
  • 数据节点(Data Node)由数据源和真实表组成的最小存储单元(如 order_db_0.order_0)
  • 绑定表(Binding Table)具有相同分片规则的关联表(如 order 和 order_item,均按 order_id 分片)
  • 广播表(Broadcast Table)所有分片都需要的公共数据(如字典表、配置表)

三、分库分表策略设计:选对 "刀" 才能切好 "蛋糕"

分库分表的成败,80% 取决于拆分策略的设计。错误的策略会导致数据分布不均、热点集中、查询复杂等问题,甚至比不拆分更糟。

3.1 分片键的选择:找到数据的 "天然分割线"

分片键是分库分表的 "基石",选错分片键会导致后续所有优化都事倍功半。优秀的分片键需满足三个条件:

  1. 高频出现在查询条件中避免无分片键的 "全表扫描"(跨所有分片查询)
  2. 数据分布均匀防止某几个分片数据量过大("数据倾斜")
  3. 业务稳定性强长期不会变更(如用户 ID 比手机号更适合,因手机号可更换)
常见分片键对比

分片键类型

适用场景

优点

缺点

示例

用户 ID

以用户为中心的业务(社交、电商)

数据分布均匀,查询目标明确

跨用户查询复杂

按 user_id 哈希分表

时间字段

时序数据(日志、订单、监控)

便于冷热数据分离,历史数据归档

可能存在热点(如秒杀活动集中在某时段)

按 order_time 分表(每月一张表)

地理区域

O2O、本地生活服务

符合业务逻辑,便于区域扩展

热门区域可能成为热点

按 city_id 分库

自定义哈希

无明显业务维度的场景

可人工控制分布均匀性

需维护哈希映射关系

对商品 ID 做哈希取模

反例:某外卖平台早期用 "商家 ID" 作为订单表分片键,导致头部商家订单集中在少数分片,单分片数据量是其他分片的 100 倍,最终被迫重构为 "用户 ID + 时间" 的复合分片键。

3.2 分片规则设计:平衡均匀性与易用性

根据分片键选择合适的拆分规则,需在 "数据均匀性" 和 "业务易用性" 之间找到平衡。

规则 1:哈希取模(Hash Mod)

原理:对分片键进行哈希计算后取模(hash(key) % 分片数),将数据分配到不同分片。

示例:按用户 ID 分 8 张表

// 计算分片索引(伪代码) int tableIndex =Math.abs(userId.hashCode())%8; // 实际表名:user_0 到 user_7 String actualTableName ="user_"+ tableIndex;

适用场景

  • 分片键为整数或可哈希的字符串(如用户 ID、商品 ID)
  • 数据访问分布均匀,无明显热点

优点

  • 数据分布均匀,计算简单
  • 扩容前查询路由明确

缺点

扩容需重分布数据(如从 8 表扩到 16 表,大部分数据需迁移)

  • 无法按范围查询(如 "查询用户 ID 1-1000 的订单" 需查所有分片)
规则 2:范围划分(Range)

原理:按分片键的数值范围拆分(如按 ID 区间、时间区间)。

示例:按订单时间分表(每月一张表)

// 计算分片索引(伪代码) LocalDate orderDate = order.getCreateTime().toLocalDate(); int year = orderDate.getYear(); int month = orderDate.getMonthValue(); // 实际表名:order_202301 到 order_202312 String actualTableName =String.format("order_%d%02d", year, month);

适用场景

  • 时序数据(订单、日志、监控指标)
  • 经常按范围查询(如 "近 3 个月订单")
  • 需要冷热数据分离(如只保留近 6 个月数据在热库)

优点

  • 扩容简单(新增时间区间的分片即可)
  • 范围查询高效,只需访问少量分片
  • 便于归档历史数据

缺点

  • 可能存在热点分片(如电商大促集中在某几个月)
  • 数据分布可能不均(有的月份订单多,有的少)
规则 3:一致性哈希(Consistent Hash)

原理:将分片和数据映射到 0-2^32 的哈希环上,数据存储在顺时针最近的分片节点。

适用场景

  • 需要频繁扩容的场景(如从 10 个分片扩到 20 个)
  • 对扩容时数据迁移量敏感的业务

优点

  • 扩容时只需迁移少量数据(受影响的只有新增节点附近的数据)
  • 可通过虚拟节点解决数据倾斜问题

缺点

  • 实现复杂,需维护哈希环
  • 范围查询效率低
规则 4:复合分片(Composite)

原理:结合两种以上规则(如先按时间范围,再按用户 ID 哈希)。

示例:订单表先按年分库,再按用户 ID 哈希分表

order_db_2023 ├─ order_0(user_id%8=0的2023年订单) ├─ order_1(user_id%8=1的2023年订单) ... └─ order_7(user_id%8=7的2023年订单) order_db_2024 ├─ order_0 ... └─ order_7

适用场景

  • 单一规则无法满足需求(如既需要按时间查询,又要避免热点)
  • 数据量极大(亿级甚至十亿级)

优点

  • 兼顾多种查询场景
  • 可有效分散热点

缺点

  • 规则复杂,增加维护成本
  • 跨复合维度查询困难

3.3 分库分表粒度:多少个分片最合适?

分片数量过少,无法解决性能问题;过多则增加复杂度和运维成本。合理的粒度设计需考虑:

  1. 单分片数据量:建议单表数据量控制在 100 万 - 500 万行(MySQL 最优性能区间)
  2. 服务器资源:单个数据库实例建议承载不超过 20 个分片表(避免连接数过高)
  3. 业务增长预期:按 1-2 年的数据增长规划分片数量(如预计 2 年数据量翻倍,初始分片数就按翻倍后设计)

计算公式总分片数 = (预计2年数据量 / 单分片最大数据量) + 20%冗余

示例:某电商订单表,当前年订单 1 亿,预计 2 年达 2 亿,单表最大 500 万行 总分片数 = 20000万 / 500万 = 40 + 20%冗余 = 48个分片 可设计为:6 个库,每个库 8 张表(6*8=48)

四、ShardingSphere 实战:Java 工程师的分库分表利器

面对分库分表的复杂性,手动处理分片路由、跨表查询几乎不可能,必须依赖成熟的中间件。Apache ShardingSphere 是目前 Java 生态最主流的分库分表解决方案,市场占有率超过 70%。

4.1 ShardingSphere 全家桶简介

ShardingSphere 包含三个核心产品,覆盖分库分表全场景:

  • ShardingSphere-JDBCJDBC 驱动层解决方案,嵌入应用内部,无需额外部署
  • ShardingSphere-Proxy代理服务器方案,独立部署,对应用透明(支持多语言)
  • ShardingSphere-SidecarServiceMesh 架构方案,适用于云原生环境

对比选择

  • 中小规模 Java 应用:优先选 ShardingSphere-JDBC(轻量、性能好)
  • 多语言混合架构:选 ShardingSphere-Proxy(统一接入点)
  • 云原生 / K8s 环境:选 ShardingSphere-Sidecar(符合云原生理念)

本文以ShardingSphere-JDBC 5.x为例(最新稳定版,支持 JDK17),讲解实战落地。

4.2 环境准备与依赖配置

开发环境
  • JDK:17
  • 数据库:MySQL 8.0
  • 框架:Spring Boot 3.1.x、MyBatis-Plus 3.5.x
  • 分库分表中间件:ShardingSphere-JDBC 5.3.2
Maven 依赖
<!-- ShardingSphere-JDBC --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.3.2</version> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency>

4.3 实战案例:订单系统分库分表设计

以电商订单系统为例,实现从单库单表到分库分表的完整方案。

业务需求
  • 日均订单 100 万,预计 2 年数据量 7 亿 +
  • 核心查询:按用户 ID 查订单、按订单时间查订单、按订单号查订单
  • 需支持订单与订单项的关联查询
  • 需保留 3 年历史数据,支持历史数据归档
拆分方案设计
  1. 分片键选择:order_id(包含用户 ID 哈希信息,便于按用户查询)
  2. 分库规则:按 order_id 哈希取模分 2 个库(order_db_0、order_db_1)
  3. 分表规则:每个库按 order_id 哈希取模分 16 张表(order_0 到 order_15)
  4. 关联表处理:订单项表(order_item)与订单表使用相同分片规则(绑定表)
  5. 字典表处理:订单状态字典表(order_status_dict)作为广播表
数据库与表结构准备

创建分库

-- 创建订单库0 CREATEDATABASEIFNOTEXISTS order_db_0 CHARACTERSET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建订单库1 CREATEDATABASEIFNOTEXISTS order_db_1 CHARACTERSET utf8mb4 COLLATE utf8mb4_unicode_ci;

创建订单表(每个库执行):-- 订单表(order_0到order_15)CREATETABLE`order_{0..15}`(`order_id`bigintNOTNULLCOMMENT'订单ID(分片键)',`user_id`bigintNOTNULLCOMMENT'用户ID',`order_no`varchar(64)NOTNULLCOMMENT'订单编号',`total_amount`decimal(10,2)NOTNULLCOMMENT'订单总金额',`status`tinyintNOTNULLCOMMENT'订单状态',`create_time`datetimeNOTNULLCOMMENT'创建时间',`update_time`datetimeNOTNULLCOMMENT'更新时间',PRIMARYKEY(`order_id`),UNIQUEKEY`uk_order_no`(`order_no`),KEY`idx_user_id`(`user_id`),KEY`idx_create_time`(`create_time`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='订单表(分表)';-- 订单项表(order_item_0到order_item_15)CREATETABLE`order_item_{0..15}`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'订单项ID',`order_id`bigintNOTNULLCOMMENT'订单ID(分片键)',`product_id`bigintNOTNULLCOMMENT'商品ID',`quantity`intNOTNULLCOMMENT'数量',`price`decimal(10,2)NOTNULLCOMMENT'单价',`create_time`datetimeNOTNULLCOMMENT'创建时间',PRIMARYKEY(`id`),KEY`idx_order_id`(`order_id`),KEY`idx_product_id`(`product_id`))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='订单项表(分表)';

创建广播表(每个库执行)

-- 订单状态字典表(所有库都有,数据一致) CREATETABLE`order_status_dict`( `status`tinyintNOTNULLCOMMENT'状态码', `name`varchar(32)NOTNULLCOMMENT'状态名称', `description`varchar(255)DEFAULTNULLCOMMENT'状态描述', PRIMARYKEY(`status`) )ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='订单状态字典(广播表)';

4.4 核心配置:ShardingSphere-JDBC 配置

# application.yml spring: shardingsphere: datasource: # 数据源名称(逻辑名称) names: order-db-0,order-db-1 # 订单库0配置 order-db-0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db_0?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: root hikari: maximum-pool-size:10 minimum-idle:5 # 订单库1配置 order-db-1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db_1?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: root hikari: maximum-pool-size:10 minimum-idle:5 # 分片规则配置 rules: sharding: # 分片算法配置 sharding-algorithms: # 分库算法(哈希取模) order-db-inline: type: INLINE props: algorithm-expression: order-db-${order_id % 2} # 分表算法(哈希取模) order-table-inline: type: INLINE props: algorithm-expression: order_${order_id % 16} # 订单项分表算法(与订单表一致) order-item-table-inline: type: INLINE props: algorithm-expression: order_item_${order_id % 16} # 数据节点配置(逻辑表 -> 数据节点) tables: # 订单表配置 order: actual-data-nodes: order-db-${0..1}.order_${0..15} database-strategy: standard: sharding-column: order_id sharding-algorithm-name: order-db-inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order-table-inline # 订单项表配置(绑定表) order_item: actual-data-nodes: order-db-${0..1}.order_item_${0..15} database-strategy: standard: sharding-column: order_id sharding-algorithm-name: order-db-inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order-item-table-inline # 绑定表配置(与order表绑定) binding-tables: order # 广播表配置 broadcast-tables: order_status_dict # 默认数据库策略(可选) default-database-strategy: none: # 默认表策略(可选) default-table-strategy: none # 属性配置 props: # 打印SQL(开发环境开启,生产环境关闭) sql-show:true # 启用SQL注释(显示分片信息) sql-comment-parse-enabled:true # MyBatis配置 mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.sharding.entity configuration: map-underscore-to-camel-case:true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

4.5 代码实现:实体类与 Mapper

实体类
// Order.java packagecom.example.sharding.entity; importcom.baomidou.mybatisplus.annotation.IdType; importcom.baomidou.mybatisplus.annotation.TableId; importcom.baomidou.mybatisplus.annotation.TableName; importlombok.Data; importjava.math.BigDecimal; importjava.time.LocalDateTime; /** * 订单实体类(逻辑表:order) */ @Data @TableName("order")// 逻辑表名 publicclassOrder{ /** * 订单ID(分片键) */ @TableId(type =IdType.INPUT) privateLong orderId; /** * 用户ID */ privateLong userId; /** * 订单编号 */ privateString orderNo; /** * 订单总金额 */ privateBigDecimal totalAmount; /** * 订单状态 */ privateInteger status; /** * 创建时间 */ privateLocalDateTime createTime; /** * 更新时间 */ privateLocalDateTime updateTime; }
// OrderItem.java packagecom.example.sharding.entity; importcom.baomidou.mybatisplus.annotation.IdType; importcom.baomidou.mybatisplus.annotation.TableId; importcom.baomidou.mybatisplus.annotation.TableName; importlombok.Data; importjava.math.BigDecimal; importjava.time.LocalDateTime; /** * 订单项实体类(逻辑表:order_item) */ @Data @TableName("order_item")// 逻辑表名 publicclassOrderItem{ /** * 订单项ID */ @TableId(type =IdType.AUTO) privateLong id; /** * 订单ID(分片键,与订单表一致) */ privateLong orderId; /** * 商品ID */ privateLong productId; /** * 数量 */ privateInteger quantity; /** * 单价 */ privateBigDecimal price; /** * 创建时间 */ privateLocalDateTime createTime; }
Mapper 接口
// OrderMapper.java packagecom.example.sharding.mapper; importcom.baomidou.mybatisplus.core.mapper.BaseMapper; importcom.example.sharding.entity.Order; importorg.apache.ibatis.annotations.Param; importorg.springframework.stereotype.Repository; importjava.time.LocalDateTime; importjava.util.List; /** * 订单Mapper */ @Repository publicinterfaceOrderMapperextendsBaseMapper<Order>{ /** * 按用户ID查询订单 */ List<Order>selectByUserId(@Param("userId")Long userId); /** * 按时间范围查询订单 */ List<Order>selectByCreateTimeBetween( @Param("startTime")LocalDateTime startTime, @Param("endTime")LocalDateTime endTime); /** * 关联查询订单与订单项 */ List<Order>selectOrderWithItems(@Param("orderId")Long orderId); }
Mapper XML(关键 SQL)
<!-- OrderMapper.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mappernamespace="com.example.sharding.mapper.OrderMapper"> <!-- 按用户ID查询订单(需注意:用户ID不是分片键,会全表扫描!优化见后文) --> <selectid="selectByUserId"resultType="com.example.sharding.entity.Order"> SELECT * FROM `order` WHERE user_id = #{userId} ORDER BY create_time DESC </select> <!-- 按时间范围查询订单(时间不是分片键,优化见后文) --> <selectid="selectByCreateTimeBetween"resultType="com.example.sharding.entity.Order"> SELECT * FROM `order` WHERE create_time BETWEEN #{startTime} AND #{endTime} ORDER BY create_time DESC </select> <!-- 关联查询订单与订单项(利用绑定表,只查询相同分片) --> <selectid="selectOrderWithItems"resultType="com.example.sharding.entity.Order"> SELECT o.*, i.id as item_id, i.product_id, i.quantity, i.price FROM `order` o LEFT JOIN order_item i ON o.order_id = i.order_id WHERE o.order_id = #{orderId} </select> </mapper>

4.6 测试代码:验证分库分表效果

// OrderServiceTest.java packagecom.example.sharding.service; importcom.example.sharding.entity.Order; importcom.example.sharding.entity.OrderItem; importcom.example.sharding.mapper.OrderItemMapper; importcom.example.sharding.mapper.OrderMapper; importorg.junit.jupiter.api.Test; importorg.slf4j.Logger; importorg.slf4j.LoggerFactory; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.boot.test.context.SpringBootTest; importjava.math.BigDecimal; importjava.time.LocalDateTime; importjava.util.List; importjava.util.Random; @SpringBootTest publicclassOrderServiceTest{ privatestaticfinalLogger logger =LoggerFactory.getLogger(OrderServiceTest.class); @Autowired privateOrderMapper orderMapper; @Autowired privateOrderItemMapper orderItemMapper; privatefinalRandom random =newRandom(); /** * 测试新增订单与订单项 */ @Test publicvoidtestInsertOrder(){ // 生成订单ID(实际应使用分布式ID生成器,如雪花算法) long orderId =System.currentTimeMillis(); long userId = random.nextLong(1000000); // 创建订单 Order order =newOrder(); order.setOrderId(orderId); order.setUserId(userId); order.setOrderNo("ORDER_"+ orderId); order.setTotalAmount(newBigDecimal("99.99")); order.setStatus(1);// 待支付 order.setCreateTime(LocalDateTime.now()); order.setUpdateTime(LocalDateTime.now()); orderMapper.insert(order); logger.info("新增订单成功:{}", orderId); // 创建订单项(与订单使用相同orderId作为分片键) OrderItem item =newOrderItem(); item.setOrderId(orderId);// 关键:使用相同的分片键 item.setProductId(random.nextLong(10000)); item.setQuantity(1); item.setPrice(newBigDecimal("99.99")); item.setCreateTime(LocalDateTime.now()); orderItemMapper.insert(item); logger.info("新增订单项成功:{}", item.getId()); } /** * 测试查询订单与订单项(绑定表查询) */ @Test publicvoidtestSelectOrderWithItems(){ long orderId =1690123456789L;// 假设已存在的订单ID List<Order> orders = orderMapper.selectOrderWithItems(orderId); logger.info("查询到订单及订单项:{}", orders); // 观察日志:SQL只会查询该orderId对应的分片,不会跨分片 } }

4.7 关键功能验证

  1. 分片路由验证:查看日志中的 SQL,确认不同 orderId 被路由到正确的库表 # 示例日志:orderId=1690123456789(1690123456789%2=1,%16=5)
代码语言:javascript
复制

Logic SQL: INSERT INTO `order` (...) VALUES (...)
Actual SQL: order-db-1 ::: INSERT INTO order_5 (...) VALUES (...)
  1. 绑定表查询验证:订单与订单项关联查询时,只访问相同分片 # 关联查询只会访问order_db_1.order_5和order_db_1.order_item_5
代码语言:javascript
复制

Logic SQL: SELECT o.*, i.id as item_id ... FROM `order` o LEFT JOIN order_item i ON o.order_id = i.order_id WHERE o.order_id = ?
Actual SQL: order-db-1 ::: SELECT o.*, i.id as item_id ... FROM order_5 o LEFT JOIN order_item_5 i ON o.order_id = i.order_id WHERE o.order_id = ?
  1. 广播表同步验证:向任意库的 order_status_dict 插入数据,其他库自动同步 -- 向order_db_0插入数据
代码语言:javascript
复制

INSERTINTO order_status_dict (status, name, description)VALUES(1,'待支付','订单创建未支付');
-- 查看order_db_1,数据已同步
SELECT*FROM order_db_1.order_status_dict;-- 能查询到刚插入的数据

五、分库分表进阶:解决实战中的 "老大难" 问题

基础的分库分表只能解决 "能跑" 的问题,要实现 "跑好",还需攻克跨分片查询、分布式事务、扩容迁移等难题。

5.1 跨分片查询优化:从 "全表扫" 到 "精准定位"

无分片键的查询(如按 user_id 查订单)会导致全分片扫描,性能极差。优化方案:

方案 1:二次索引表(推荐)

原理:建立分片键与非分片键的映射关系表,将无分片键查询转为有分片键查询。

示例:用户订单索引表

-- 用户订单索引表(按user_id分表,存储user_id与order_id的映射) CREATETABLE`user_order_index_${0..7}`( `id`bigintNOTNULLAUTO_INCREMENT, `user_id`bigintNOTNULLCOMMENT'用户ID(分片键)', `order_id`bigintNOTNULLCOMMENT'订单ID', `create_time`datetimeNOTNULL, PRIMARYKEY(`id`), KEY`idx_user_id_create_time`(`user_id`,`create_time`) )ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户订单索引表';

查询流程

  1. 按 user_id 查询索引表,获取该用户的所有 order_id
  2. 按 order_id 查询订单表(有分片键,精准路由)

代码实现

// 查询用户订单优化版 publicList<Order>selectByUserIdOptimized(Long userId){ // 1. 查询索引表获取order_id列表(按user_id分片,精准查询) List<Long> orderIds = userOrderIndexMapper.selectOrderIdsByUserId(userId); if(orderIds.isEmpty()){ returnCollections.emptyList(); } // 2. 按order_id列表查询订单(有分片键,批量路由) return orderMapper.selectBatchIds(orderIds); }

方案 2:分片键包含非分片键信息

原理:在生成分片键时嵌入非分片键信息(如 order_id 前 4 位为 user_id 的哈希值)。

// 生成包含user_id信息的order_id(伪代码) publiclonggenerateOrderId(Long userId){ // 取user_id哈希值的前4位(16进制) String userIdHash =Integer.toHexString(Math.abs(userId.hashCode())).substring(0,4); // 结合雪花算法生成的ID long snowflakeId = snowflakeGenerator.nextId(); // 拼接:前4位是user_id哈希,后面是雪花ID returnLong.parseLong(userIdHash + snowflakeId); }

优点:无需额外表,可直接从 order_id 解析出 user_id 相关的分片信息

缺点:ID 生成逻辑复杂,扩展性差(规则变更需重构)

5.2 分布式事务:保证跨库操作的一致性

分库分表后,跨库操作无法依赖 MySQL 的本地事务,需采用分布式事务方案。

方案 1:Seata AT 模式(推荐)

Seata 是阿里开源的分布式事务解决方案,AT 模式对业务侵入小,适合大多数场景。

集成步骤

  1. 引入 Seata 依赖

<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.7.1</version> </dependency>

  1. 配置 Seata seata:
代码语言:javascript
复制

tx-service-group: my_test_tx_group
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1:8091
registry:
type: file
  1. 业务代码中添加 @GlobalTransactional 注解 /**
代码语言:javascript
复制

 * 创建订单(跨库操作,需分布式事务)
 */
@GlobalTransactional(rollbackFor =Exception.class)
publicvoidcreateOrder(OrderCreateDTO dto){
// 1. 创建订单(order_db_0)
Order order =buildOrder(dto);
    orderMapper.insert(order);

// 2. 扣减库存(product_db,另一数据库)
    productService.reduceStock(dto.getProductId(), dto.getQuantity());

// 3. 记录日志(log_db,另一数据库)
    logService.recordOrderLog(order.getOrderId(),"订单创建成功");
}
方案 2:最终一致性方案(高并发场景)

对于秒杀等高并发场景,可采用 "本地消息表 + 定时任务" 实现最终一致性:

  1. 本地事务:更新业务表 + 插入消息表
  2. 定时任务:扫描消息表,发送消息到 MQ
  3. 消费端:接收消息,执行跨库操作,确认消息

优点:性能好,无锁阻塞

缺点:实现复杂,需处理消息重复、失败重试等问题

5.3 分库分表扩容:从 8 表到 16 表的平滑迁移

业务增长超预期时,需扩容分片数量(如从 8 表扩到 16 表),关键是避免停机减少数据迁移量

平滑扩容四步法:
  1. 双写准备:修改应用,同时向旧分片和新分片写入数据(新分片按新规则计算)
  2. 数据同步:用工具(如 ShardingSphere-Scaling)将旧分片数据同步到新分片
  3. 流量切换:逐步将读流量切换到新分片,验证数据一致性
  4. 清理下线:确认新分片稳定后,停止写旧分片,清理旧数据

代码示例:双写逻辑

// 双写版本的订单插入 publicvoidinsertOrderWithDoubleWrite(Order order){ // 1. 按旧规则写入旧分片 int oldTableIndex =(int)(order.getOrderId()%8); String oldTableName ="order_"+ oldTableIndex; orderMapper.insertIntoSpecificTable(oldTableName, order); // 2. 按新规则写入新分片 int newTableIndex =(int)(order.getOrderId()%16); String newTableName ="order_"+ newTableIndex; orderMapper.insertIntoSpecificTable(newTableName, order); }

注意事项

  • 扩容时尽量选择 2 的倍数(如 8→16,16→32),减少数据迁移量
  • 迁移过程中需监控新旧分片的数据一致性(可通过 MD5 校验)
  • 读流量切换可按比例逐步进行(10%→50%→100%)

5.4 分页查询优化:避免 "跨分片 Limit" 陷阱

分库分表后,分页查询(如LIMIT 100000, 10)会导致严重性能问题:每个分片都需查询前 100010 条数据,再汇总排序取 10 条。

优化方案

  1. "分片键 + 分页条件" 精准定位: // 优化:按order_id(分片键)和create_time分页
代码语言:javascript
复制

publicIPage<Order>selectByPage(IPage<Order> page,Long lastOrderId,LocalDateTime lastCreateTime){
QueryWrapper<Order> wrapper =newQueryWrapper<>();
// 利用分片键和上次分页的最后一条记录做条件
    wrapper.gt("order_id", lastOrderId)
.gt("create_time", lastCreateTime)
.orderByAsc("order_id")
.last("LIMIT "+ page.getSize());
return orderMapper.selectPage(page, wrapper);
}
  1. 二次查询法
    • 第一次:查询各分片的符合条件的总记录数,计算分页偏移量
    • 第二次:按计算后的偏移量查询各分片,汇总结果
  2. 禁止深分页
    • 业务上限制最大分页页码(如最多支持 100 页)
    • 提供 "加载更多" 而非页码跳转的交互方式

六、分库分表避坑指南:20 + 生产环境常见问题

分库分表涉及复杂的分布式逻辑,生产环境中容易踩坑。以下是经过实战验证的避坑指南:

6.1 设计阶段的坑

  1. 分片键选择随意:用 "订单号" 而非 "用户 ID" 作为分片键,导致 "查询用户所有订单" 需全表扫描
    • 避坑:优先选择 "查询频率最高、分布最均匀" 的字段作为分片键
  2. 过度拆分:将本应关联的表拆到不同分片,导致联表查询必须跨分片
    • 避坑:关联密切的表(如 order 和 order_item)必须使用相同分片键,设为绑定表
  3. 忽略数据增长趋势:按当前数据量设计分片,1 年后就需紧急扩容
    • 避坑:至少按 1-2 年的增长预期设计分片数量,预留 20% 冗余
  4. 分片规则过于复杂:使用多层嵌套的自定义分片算法,难以维护和排查问题
    • 避坑:优先使用内置分片算法(如 INLINE、MOD),自定义算法需严格测试

6.2 开发阶段的坑

  1. 硬编码表名:SQL 中写死真实表名(如 order_0),而非逻辑表名(order)
    • 避坑:所有 SQL 必须使用逻辑表名,由中间件自动路由到真实表
  2. 不使用分布式 ID:继续使用自增 ID 作为主键,导致主键冲突
    • 避坑:必须使用分布式 ID 生成器(如雪花算法),保证主键全局唯一
  3. ** 滥用 SELECT ***:查询所有字段,包括不需要的大字段,增加网络传输和内存消耗
    • 避坑:明确指定需要的字段,尤其是分表后单表数据量仍较大的场景
  4. 不处理分片键为 NULL 的情况:导致 NULL 值被路由到固定分片,造成数据倾斜
    • 避坑:分片键必须非空,业务代码中做非空校验,数据库表设置 NOT NULL

6.3 运维阶段的坑

  1. DDL 操作不规范:直接对单个分片执行 ALTER TABLE,导致各分片表结构不一致
    • 避坑:通过中间件执行 DDL(如 ShardingSphere-Proxy 的 DISTSQL),自动同步到所有分片
  2. 监控不到位:只监控数据库整体性能,忽略单个分片的热点
    • 避坑:按分片维度监控(QPS、响应时间、数据量),设置单个分片的告警阈值
  3. 备份策略错误:只备份部分分片,或各分片备份时间不一致
    • 避坑:所有分片同时备份,备份文件包含分片标识,定期演练跨分片恢复
  4. 忽略数据归档:历史数据长期保留在热库,导致分片数据量持续增长
    • 避坑:按时间维度(如 3 个月前的数据)归档到冷存储,查询时自动路由

七、总结:分库分表的 "道" 与 "术"

分库分表不是银弹,而是一把双刃剑:用好了能解决亿级数据的存储难题,用不好则会引入更多复杂性。作为 Java 工程师,需掌握其 "道" 与 "术":

  • "道":理解分库分表的本质是 "水平扩展",核心是 "平衡数据分布与查询效率",最终目标是支撑业务增长。
  • "术":熟练运用 ShardingSphere 等工具,掌握分片键选择、规则设计、跨分片查询优化等实战技巧。

分库分表成熟度 Checklist

  • 初级:能基于中间件实现基本的分库分表,解决数据量过大问题
  • 中级:能优化跨分片查询,处理分布式事务,实现平滑扩容
  • 高级:能根据业务特性设计混合分片策略,平衡性能与复杂度,构建自动化运维体系

最后,记住分库分表是业务驱动的技术方案,而非技术驱动的炫技。在数据量未达到瓶颈前,不要过早引入 —— 简单的单库单表加索引,往往比复杂的分库分表更高效。

希望本文能帮你在分库分表的实战中少走弯路,让你的系统在数据爆炸时代依然能 "稳如泰山"!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么分库分表是 Java 工程师的必修课?
    • 1.1 单库单表的极限在哪里?
    • 1.2 分库分表的核心价值
    • 1.3 分库分表的三大误区
  • 二、分库分表核心概念:从 "拆什么" 到 "怎么拆"
    • 2.1 垂直拆分 vs 水平拆分
      • 垂直拆分:按业务领域切割
      • 水平拆分:按数据行切割
    • 2.2 分库分表的关键术语
  • 三、分库分表策略设计:选对 "刀" 才能切好 "蛋糕"
    • 3.1 分片键的选择:找到数据的 "天然分割线"
      • 常见分片键对比
    • 3.2 分片规则设计:平衡均匀性与易用性
      • 规则 1:哈希取模(Hash Mod)
      • 规则 2:范围划分(Range)
      • 规则 3:一致性哈希(Consistent Hash)
      • 规则 4:复合分片(Composite)
    • 3.3 分库分表粒度:多少个分片最合适?
  • 四、ShardingSphere 实战:Java 工程师的分库分表利器
    • 4.1 ShardingSphere 全家桶简介
    • 4.2 环境准备与依赖配置
      • 开发环境
      • Maven 依赖
      • <!-- ShardingSphere-JDBC --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.3.2</version> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency>
    • 4.3 实战案例:订单系统分库分表设计
      • 业务需求
      • 拆分方案设计
      • 数据库与表结构准备
    • 4.4 核心配置:ShardingSphere-JDBC 配置
    • # application.yml spring: shardingsphere: datasource: # 数据源名称(逻辑名称) names: order-db-0,order-db-1 # 订单库0配置 order-db-0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db_0?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: root hikari: maximum-pool-size:10 minimum-idle:5 # 订单库1配置 order-db-1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db_1?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: root hikari: maximum-pool-size:10 minimum-idle:5 # 分片规则配置 rules: sharding: # 分片算法配置 sharding-algorithms: # 分库算法(哈希取模) order-db-inline: type: INLINE props: algorithm-expression: order-db-${order_id % 2} # 分表算法(哈希取模) order-table-inline: type: INLINE props: algorithm-expression: order_${order_id % 16} # 订单项分表算法(与订单表一致) order-item-table-inline: type: INLINE props: algorithm-expression: order_item_${order_id % 16} # 数据节点配置(逻辑表 -> 数据节点) tables: # 订单表配置 order: actual-data-nodes: order-db-${0..1}.order_${0..15} database-strategy: standard: sharding-column: order_id sharding-algorithm-name: order-db-inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order-table-inline # 订单项表配置(绑定表) order_item: actual-data-nodes: order-db-${0..1}.order_item_${0..15} database-strategy: standard: sharding-column: order_id sharding-algorithm-name: order-db-inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order-item-table-inline # 绑定表配置(与order表绑定) binding-tables: order # 广播表配置 broadcast-tables: order_status_dict # 默认数据库策略(可选) default-database-strategy: none: # 默认表策略(可选) default-table-strategy: none # 属性配置 props: # 打印SQL(开发环境开启,生产环境关闭) sql-show:true # 启用SQL注释(显示分片信息) sql-comment-parse-enabled:true # MyBatis配置 mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.sharding.entity configuration: map-underscore-to-camel-case:true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
    • 4.5 代码实现:实体类与 Mapper
      • 实体类
      • // Order.java packagecom.example.sharding.entity; importcom.baomidou.mybatisplus.annotation.IdType; importcom.baomidou.mybatisplus.annotation.TableId; importcom.baomidou.mybatisplus.annotation.TableName; importlombok.Data; importjava.math.BigDecimal; importjava.time.LocalDateTime; /** * 订单实体类(逻辑表:order) */ @Data @TableName("order")// 逻辑表名 publicclassOrder{ /** * 订单ID(分片键) */ @TableId(type =IdType.INPUT) privateLong orderId; /** * 用户ID */ privateLong userId; /** * 订单编号 */ privateString orderNo; /** * 订单总金额 */ privateBigDecimal totalAmount; /** * 订单状态 */ privateInteger status; /** * 创建时间 */ privateLocalDateTime createTime; /** * 更新时间 */ privateLocalDateTime updateTime; }
      • // OrderItem.java packagecom.example.sharding.entity; importcom.baomidou.mybatisplus.annotation.IdType; importcom.baomidou.mybatisplus.annotation.TableId; importcom.baomidou.mybatisplus.annotation.TableName; importlombok.Data; importjava.math.BigDecimal; importjava.time.LocalDateTime; /** * 订单项实体类(逻辑表:order_item) */ @Data @TableName("order_item")// 逻辑表名 publicclassOrderItem{ /** * 订单项ID */ @TableId(type =IdType.AUTO) privateLong id; /** * 订单ID(分片键,与订单表一致) */ privateLong orderId; /** * 商品ID */ privateLong productId; /** * 数量 */ privateInteger quantity; /** * 单价 */ privateBigDecimal price; /** * 创建时间 */ privateLocalDateTime createTime; }
      • Mapper 接口
      • // OrderMapper.java packagecom.example.sharding.mapper; importcom.baomidou.mybatisplus.core.mapper.BaseMapper; importcom.example.sharding.entity.Order; importorg.apache.ibatis.annotations.Param; importorg.springframework.stereotype.Repository; importjava.time.LocalDateTime; importjava.util.List; /** * 订单Mapper */ @Repository publicinterfaceOrderMapperextendsBaseMapper<Order>{ /** * 按用户ID查询订单 */ List<Order>selectByUserId(@Param("userId")Long userId); /** * 按时间范围查询订单 */ List<Order>selectByCreateTimeBetween( @Param("startTime")LocalDateTime startTime, @Param("endTime")LocalDateTime endTime); /** * 关联查询订单与订单项 */ List<Order>selectOrderWithItems(@Param("orderId")Long orderId); }
      • Mapper XML(关键 SQL)
      • <!-- OrderMapper.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mappernamespace="com.example.sharding.mapper.OrderMapper"> <!-- 按用户ID查询订单(需注意:用户ID不是分片键,会全表扫描!优化见后文) --> <selectid="selectByUserId"resultType="com.example.sharding.entity.Order"> SELECT * FROM `order` WHERE user_id = #{userId} ORDER BY create_time DESC </select> <!-- 按时间范围查询订单(时间不是分片键,优化见后文) --> <selectid="selectByCreateTimeBetween"resultType="com.example.sharding.entity.Order"> SELECT * FROM `order` WHERE create_time BETWEEN #{startTime} AND #{endTime} ORDER BY create_time DESC </select> <!-- 关联查询订单与订单项(利用绑定表,只查询相同分片) --> <selectid="selectOrderWithItems"resultType="com.example.sharding.entity.Order"> SELECT o.*, i.id as item_id, i.product_id, i.quantity, i.price FROM `order` o LEFT JOIN order_item i ON o.order_id = i.order_id WHERE o.order_id = #{orderId} </select> </mapper>
    • 4.6 测试代码:验证分库分表效果
    • // OrderServiceTest.java packagecom.example.sharding.service; importcom.example.sharding.entity.Order; importcom.example.sharding.entity.OrderItem; importcom.example.sharding.mapper.OrderItemMapper; importcom.example.sharding.mapper.OrderMapper; importorg.junit.jupiter.api.Test; importorg.slf4j.Logger; importorg.slf4j.LoggerFactory; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.boot.test.context.SpringBootTest; importjava.math.BigDecimal; importjava.time.LocalDateTime; importjava.util.List; importjava.util.Random; @SpringBootTest publicclassOrderServiceTest{ privatestaticfinalLogger logger =LoggerFactory.getLogger(OrderServiceTest.class); @Autowired privateOrderMapper orderMapper; @Autowired privateOrderItemMapper orderItemMapper; privatefinalRandom random =newRandom(); /** * 测试新增订单与订单项 */ @Test publicvoidtestInsertOrder(){ // 生成订单ID(实际应使用分布式ID生成器,如雪花算法) long orderId =System.currentTimeMillis(); long userId = random.nextLong(1000000); // 创建订单 Order order =newOrder(); order.setOrderId(orderId); order.setUserId(userId); order.setOrderNo("ORDER_"+ orderId); order.setTotalAmount(newBigDecimal("99.99")); order.setStatus(1);// 待支付 order.setCreateTime(LocalDateTime.now()); order.setUpdateTime(LocalDateTime.now()); orderMapper.insert(order); logger.info("新增订单成功:{}", orderId); // 创建订单项(与订单使用相同orderId作为分片键) OrderItem item =newOrderItem(); item.setOrderId(orderId);// 关键:使用相同的分片键 item.setProductId(random.nextLong(10000)); item.setQuantity(1); item.setPrice(newBigDecimal("99.99")); item.setCreateTime(LocalDateTime.now()); orderItemMapper.insert(item); logger.info("新增订单项成功:{}", item.getId()); } /** * 测试查询订单与订单项(绑定表查询) */ @Test publicvoidtestSelectOrderWithItems(){ long orderId =1690123456789L;// 假设已存在的订单ID List<Order> orders = orderMapper.selectOrderWithItems(orderId); logger.info("查询到订单及订单项:{}", orders); // 观察日志:SQL只会查询该orderId对应的分片,不会跨分片 } }
    • 4.7 关键功能验证
  • 五、分库分表进阶:解决实战中的 "老大难" 问题
    • 5.1 跨分片查询优化:从 "全表扫" 到 "精准定位"
      • 方案 1:二次索引表(推荐)
      • 方案 2:分片键包含非分片键信息
    • 5.2 分布式事务:保证跨库操作的一致性
      • 方案 1:Seata AT 模式(推荐)
      • 方案 2:最终一致性方案(高并发场景)
    • 5.3 分库分表扩容:从 8 表到 16 表的平滑迁移
      • 平滑扩容四步法:
    • 5.4 分页查询优化:避免 "跨分片 Limit" 陷阱
  • 六、分库分表避坑指南:20 + 生产环境常见问题
    • 6.1 设计阶段的坑
    • 6.2 开发阶段的坑
    • 6.3 运维阶段的坑
  • 七、总结:分库分表的 "道" 与 "术"
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档