
当你的业务从日活百万跃升到千万,数据库单表数据量突破 5000 万行时,是否遇到过这些噩梦:简单查询耗时从毫秒级飙升到秒级,索引优化杯水车薪,DDL 操作导致全表锁死,服务器 IO 被频繁的磁盘扫描占满?
我深耕 Java 后端十余年,亲历过三次从单库单表到分库分表的架构迁移,见证过因拆分策略失误导致的 "二次重构",也踩过分布式事务、跨表查询的各种深坑。本文将系统拆解分库分表的底层逻辑,结合 ShardingSphere 实战案例,手把手教你实现从 "卡成 PPT" 到 "亿级数据秒查" 的蜕变,附带 10 + 生产级代码示例和 20 + 避坑指南。
在互联网业务爆发式增长的今天,"数据量" 和 "访问量" 的双重爆炸正在瓦解传统单库单表的根基。理解分库分表的必要性,首先要认清单库单表的三大死穴。
MySQL 作为最主流的关系型数据库,在单库单表场景下存在明确的性能临界点:
指标 | 安全阈值 | 危险阈值 | 崩溃阈值 |
|---|---|---|---|
单表数据量 | <1000 万行 | 1000 万 - 5000 万行 | >5000 万行 |
日均新增数据 | <100 万行 | 100 万 - 500 万行 | >500 万行 |
单表索引数量 | <5 个 | 5-10 个 | >10 个 |
每秒查询量 (QPS) | <1000 | 1000-5000 | >5000 |
超过安全阈值后,会出现一系列难以解决的问题:
某电商平台真实案例:订单表数据量达 8000 万行后,"查询用户近 3 个月订单" 接口响应时间从 300ms 增至 5s+,每日因超时导致的订单投诉增长 300%,数据库服务器 IO 利用率持续 95% 以上,最终被迫紧急停机分表。
分库分表(Sharding)通过 "拆分" 打破单库单表的物理限制,本质是一种 "分而治之" 的水平扩展策略:
经过验证的性能收益:某支付系统分库分表后,单表数据量从 6000 万降至 500 万,查询响应时间降低 92%,写入 TPS 提升 7 倍,年服务器成本降低 40%(用 10 台普通服务器替代 2 台小型机)。
很多团队在分库分表时存在认知偏差,导致从一开始就埋下隐患:
正确的做法是:提前规划,按需拆分,因业务制宜。在数据量达到安全阈值的 70% 时就启动分库分表设计,根据业务访问模式选择最合适的拆分策略。
分库分表不是简单的 "一拆了之",需要先理解其底层逻辑和核心术语,避免在实战中 "知其然不知其所以然"。
分库分表的两种基本拆分方式,适用场景截然不同:
核心思想:将单一数据库或表按业务功能拆分到不同的库或表,实现 "专库专表"。
垂直分库:按业务模块拆分数据库(如用户库、订单库、商品库)
原单库:mall_db
拆分后:
- user_db(用户相关表)
- order_db(订单相关表)
- product_db(商品相关表)
垂直分表:将大表按字段冷热拆分(如将用户表拆分为基本信息表和详情表)
-- 原大表
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,
...
);
适用场景:
优点:
缺点:
核心思想:将大表的行数据按规则分散到多个结构相同的表或库中,实现 "数据分片"。
适用场景:
优点:
缺点:
理解这些术语是掌握分库分表工具的基础:
分库分表的成败,80% 取决于拆分策略的设计。错误的策略会导致数据分布不均、热点集中、查询复杂等问题,甚至比不拆分更糟。
分片键是分库分表的 "基石",选错分片键会导致后续所有优化都事倍功半。优秀的分片键需满足三个条件:
分片键类型 | 适用场景 | 优点 | 缺点 | 示例 |
|---|---|---|---|---|
用户 ID | 以用户为中心的业务(社交、电商) | 数据分布均匀,查询目标明确 | 跨用户查询复杂 | 按 user_id 哈希分表 |
时间字段 | 时序数据(日志、订单、监控) | 便于冷热数据分离,历史数据归档 | 可能存在热点(如秒杀活动集中在某时段) | 按 order_time 分表(每月一张表) |
地理区域 | O2O、本地生活服务 | 符合业务逻辑,便于区域扩展 | 热门区域可能成为热点 | 按 city_id 分库 |
自定义哈希 | 无明显业务维度的场景 | 可人工控制分布均匀性 | 需维护哈希映射关系 | 对商品 ID 做哈希取模 |
反例:某外卖平台早期用 "商家 ID" 作为订单表分片键,导致头部商家订单集中在少数分片,单分片数据量是其他分片的 100 倍,最终被迫重构为 "用户 ID + 时间" 的复合分片键。
根据分片键选择合适的拆分规则,需在 "数据均匀性" 和 "业务易用性" 之间找到平衡。
原理:对分片键进行哈希计算后取模(hash(key) % 分片数),将数据分配到不同分片。
示例:按用户 ID 分 8 张表
// 计算分片索引(伪代码)
int tableIndex =Math.abs(userId.hashCode())%8;
// 实际表名:user_0 到 user_7
String actualTableName ="user_"+ tableIndex;
适用场景:
优点:
缺点:
扩容需重分布数据(如从 8 表扩到 16 表,大部分数据需迁移)
原理:按分片键的数值范围拆分(如按 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);
适用场景:
优点:
缺点:
原理:将分片和数据映射到 0-2^32 的哈希环上,数据存储在顺时针最近的分片节点。
适用场景:
优点:
缺点:
原理:结合两种以上规则(如先按时间范围,再按用户 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
适用场景:
优点:
缺点:
分片数量过少,无法解决性能问题;过多则增加复杂度和运维成本。合理的粒度设计需考虑:
计算公式:
总分片数 = (预计2年数据量 / 单分片最大数据量) + 20%冗余
示例:某电商订单表,当前年订单 1 亿,预计 2 年达 2 亿,单表最大 500 万行
总分片数 = 20000万 / 500万 = 40 + 20%冗余 = 48个分片
可设计为:6 个库,每个库 8 张表(6*8=48)
面对分库分表的复杂性,手动处理分片路由、跨表查询几乎不可能,必须依赖成熟的中间件。Apache ShardingSphere 是目前 Java 生态最主流的分库分表解决方案,市场占有率超过 70%。
ShardingSphere 包含三个核心产品,覆盖分库分表全场景:
对比选择:
本文以ShardingSphere-JDBC 5.x为例(最新稳定版,支持 JDK17),讲解实战落地。
<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>
以电商订单系统为例,实现从单库单表到分库分表的完整方案。
创建分库:
-- 创建订单库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='订单状态字典(广播表)';
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
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;
}
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);
}
<?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>
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对应的分片,不会跨分片
}
}
Logic SQL: INSERT INTO `order` (...) VALUES (...)
Actual SQL: order-db-1 ::: INSERT INTO order_5 (...) VALUES (...)
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 = ?
INSERTINTO order_status_dict (status, name, description)VALUES(1,'待支付','订单创建未支付');
-- 查看order_db_1,数据已同步
SELECT*FROM order_db_1.order_status_dict;-- 能查询到刚插入的数据
基础的分库分表只能解决 "能跑" 的问题,要实现 "跑好",还需攻克跨分片查询、分布式事务、扩容迁移等难题。
无分片键的查询(如按 user_id 查订单)会导致全分片扫描,性能极差。优化方案:
原理:建立分片键与非分片键的映射关系表,将无分片键查询转为有分片键查询。
示例:用户订单索引表
-- 用户订单索引表(按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='用户订单索引表';
查询流程:
代码实现:
// 查询用户订单优化版
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);
}
原理:在生成分片键时嵌入非分片键信息(如 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 生成逻辑复杂,扩展性差(规则变更需重构)
分库分表后,跨库操作无法依赖 MySQL 的本地事务,需采用分布式事务方案。
Seata 是阿里开源的分布式事务解决方案,AT 模式对业务侵入小,适合大多数场景。
集成步骤:
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
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
* 创建订单(跨库操作,需分布式事务)
*/
@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(),"订单创建成功");
}
对于秒杀等高并发场景,可采用 "本地消息表 + 定时任务" 实现最终一致性:
优点:性能好,无锁阻塞
缺点:实现复杂,需处理消息重复、失败重试等问题
业务增长超预期时,需扩容分片数量(如从 8 表扩到 16 表),关键是避免停机和减少数据迁移量。
代码示例:双写逻辑
// 双写版本的订单插入
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);
}
注意事项:
分库分表后,分页查询(如LIMIT 100000, 10)会导致严重性能问题:每个分片都需查询前 100010 条数据,再汇总排序取 10 条。
优化方案:
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);
}
分库分表涉及复杂的分布式逻辑,生产环境中容易踩坑。以下是经过实战验证的避坑指南:
分库分表不是银弹,而是一把双刃剑:用好了能解决亿级数据的存储难题,用不好则会引入更多复杂性。作为 Java 工程师,需掌握其 "道" 与 "术":
分库分表成熟度 Checklist:
最后,记住分库分表是业务驱动的技术方案,而非技术驱动的炫技。在数据量未达到瓶颈前,不要过早引入 —— 简单的单库单表加索引,往往比复杂的分库分表更高效。
希望本文能帮你在分库分表的实战中少走弯路,让你的系统在数据爆炸时代依然能 "稳如泰山"!