梅特尔恐怖逃生
64.25M · 2026-03-22
在互联网高并发、大数据量的业务场景下,单库单表的存储模式会面临性能瓶颈 —— 数据量达到千万级后,磁盘 IO、索引查询、事务处理的效率会急剧下降。分库分表是解决这一问题的核心方案,它通过将数据分散存储到多个数据库或数据表中,提升系统的吞吐量和可用性。
本文将从 What-Why-How 三个维度深度解析 MySQL 分库分表,结合实战案例代码,帮助开发者理解并落地这一技术方案。
分库分表是水平拆分和垂直拆分的统称,本质是将单一数据库或数据表的数据,按照特定规则分散到多个物理节点上,从而降低单节点的数据压力。
垂直拆分是按照业务维度进行拆分,核心思想是 "专库专用"。
t_user 可拆分为基础信息表 t_user_base 和详情信息表 t_user_ext。适用场景:表中字段过多,部分字段访问频率低,或不同字段归属不同业务模块。优势:业务隔离,便于维护;降低单表字段数量,提升查询效率。缺点:无法解决单表数据量过大的问题。
水平拆分是按照数据维度进行拆分,核心思想是 "数据分片"。
t_order 拆分为 db_order_0 到 db_order_3 四个库,每个库都有 t_order 表。t_order 拆分为 t_order_0 到 t_order_7 八个表。适用场景:单表数据量巨大(千万级以上),查询和写入性能瓶颈明显。优势:突破单表数据量上限,提升并发读写能力。缺点:引入分布式事务、跨分片查询等复杂问题。
单库单表的性能瓶颈主要体现在以下几个方面,分库分表是解决这些问题的必经之路:
核心目标:提升系统的并发承载能力和数据存储能力,保障业务稳定运行。
分片规则是分库分表的核心,决定了数据如何分配到不同的分片节点。常用的分片规则有以下几种:
| 分片规则 | 原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 范围分片 | 按数据的范围区间划分,例如按订单创建时间 create_time 分为 2024 年、2025 年分片 | 时间序列数据,如订单、日志 | 规则简单,便于扩容 | 数据热点问题,新分片可能集中承载大量写入 |
| 哈希分片 | 对分片键(如 user_id)进行哈希计算,取模后映射到分片 | 用户 ID、订单 ID 等均匀分布的数据 | 数据分布均匀,避免热点 | 扩容时需要迁移数据,复杂度高 |
| 列表分片 | 按分片键的枚举值划分,例如按省份 province 分为北京、上海、广东分片 | 数据具有明确枚举属性的场景 | 规则直观,便于业务隔离 | 分片数量固定,扩展灵活度低 |
| 复合分片 | 组合多种规则,例如先按时间范围分片,再按用户 ID 哈希分片 | 复杂业务场景,如海量订单系统 | 兼顾多种规则的优势 | 规则复杂,维护成本高 |
分片键是决定数据分片的字段,选择合适的分片键是分库分表的关键,需遵循以下原则:
user_id、order_id。Sharding-JDBC 是一款轻量级的分库分表框架,它基于 JDBC 层实现,无需独立部署中间件,对业务代码侵入性低,是开发者首选的分库分表方案。
在 pom.xml 中引入核心依赖:
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- Sharding-JDBC -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.3.2</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
以电商订单系统为例,实现以下目标:
user_id 进行水平分库:分为 2 个库 db_order_0、db_order_1。order_id 进行水平分表:每个库中分为 4 个表 t_order_0-t_order_3。user_id % 2 = 库索引,order_id % 4 = 表索引。创建 2 个分库,每个库创建 4 个分表,表结构完全一致。
-- 创建分库 db_order_0
CREATE DATABASE IF NOT EXISTS db_order_0 DEFAULT CHARACTER SET utf8mb4;
-- 创建分库 db_order_1
CREATE DATABASE IF NOT EXISTS db_order_1 DEFAULT CHARACTER SET utf8mb4;
-- 切换到 db_order_0
USE db_order_0;
-- 创建订单分表 t_order_0
CREATE TABLE t_order_0 (
order_id BIGINT PRIMARY KEY COMMENT '订单ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
order_amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
create_time DATETIME NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 同理,在 db_order_0 创建 t_order_1、t_order_2、t_order_3
-- 在 db_order_1 中创建 t_order_0、t_order_1、t_order_2、t_order_3
在 application.yml 中配置数据源、分片规则、主键生成策略等核心参数:
spring:
# Sharding-JDBC 配置
shardingsphere:
# 数据源配置
datasource:
# 数据源名称列表
names: db_order_0,db_order_1
# 配置 db_order_0 数据源
db_order_0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db_order_0?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: root
# 配置 db_order_1 数据源
db_order_1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db_order_1?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: root
# 规则配置
rules:
sharding:
# 分片算法配置
sharding-algorithms:
# 分库算法:基于 user_id 取模
db-mod-algorithm:
type: INLINE
props:
algorithm-expression: db_order_${user_id % 2}
# 分表算法:基于 order_id 取模
table-mod-algorithm:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 4}
# 表规则配置
tables:
# 逻辑表名称
t_order:
# 数据节点:分库.分表
actual-data-nodes: db_order_${0..1}.t_order_${0..3}
# 分库策略
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-mod-algorithm
# 分表策略
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: table-mod-algorithm
# 主键生成策略
key-generate-strategy:
column: order_id
key-generator-name: snowflake
# 主键生成器
key-generators:
snowflake:
type: SNOWFLAKE
# 属性配置
props:
# 打印 SQL 语句,便于调试
sql-show: true
# MyBatis Plus 配置
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.stdout.StdOutImpl
Order.javapackage com.example.sharding.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("t_order") // 对应逻辑表名称
public class Order {
/**
* 订单ID,雪花算法生成
*/
private Long orderId;
/**
* 用户ID,分库键
*/
private Long userId;
/**
* 订单金额
*/
private BigDecimal orderAmount;
/**
* 创建时间
*/
private Date createTime;
}
OrderMapper.javapackage com.example.sharding.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.sharding.entity.Order;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
OrderService.javapackage com.example.sharding.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.sharding.entity.Order;
import com.example.sharding.mapper.OrderMapper;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class OrderService extends ServiceImpl<OrderMapper, Order> {
/**
* 创建订单
*/
public boolean createOrder(Long userId, Double amount) {
Order order = new Order();
order.setUserId(userId);
order.setOrderAmount(BigDecimal.valueOf(amount));
order.setCreateTime(new Date());
return save(order);
}
}
OrderController.javapackage com.example.sharding.controller;
import com.example.sharding.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 测试创建订单接口
*/
@GetMapping("/order/create")
public String createOrder(@RequestParam Long userId, @RequestParam Double amount) {
boolean result = orderService.createOrder(userId, amount);
return result ? "订单创建成功" : "订单创建失败";
}
}
启动 Spring Boot 项目,调用接口进行测试:
# 测试 user_id=1(模2=1,对应 db_order_1)
curl "http://localhost:8080/order/create?userId=1&amount=100.0"
# 测试 user_id=2(模2=0,对应 db_order_0)
curl "http://localhost:8080/order/create?userId=2&amount=200.0"
查看控制台打印的 SQL 语句,可以看到 Sharding-JDBC 自动路由到了对应的分库分表:
-- user_id=1 时,路由到 db_order_1.t_order_x
INSERT INTO t_order_1 (order_id, user_id, order_amount, create_time) VALUES (?, ?, ?, ?)
-- user_id=2 时,路由到 db_order_0.t_order_y
INSERT INTO t_order_2 (order_id, user_id, order_amount, create_time) VALUES (?, ?, ?, ?)
分库分表虽然解决了性能瓶颈,但也引入了分布式场景下的复杂问题,以下是常见问题及应对方案:
问题:跨库操作时,无法保证事务的 ACID 特性,例如用户下单时需要同时操作订单库和库存库。解决方案:
问题:按非分片键查询时,需要扫描所有分片,性能低下。解决方案:
问题:哈希分片扩容时,数据分布规则改变,需要迁移大量数据。解决方案:
问题:分库分表后,读压力依然较大。解决方案:
application.yml 中配置主从数据源。分库分表是解决 MySQL 性能瓶颈的核心方案,但它不是 "银弹"—— 它带来了复杂度的提升,需要开发者在性能和复杂度之间做权衡。
在实际项目中,应遵循 "业务驱动技术" 的原则:先通过索引优化、SQL 优化、缓存优化等手段提升性能,当这些手段无法满足需求时,再考虑分库分表。
本文提供的 Sharding-JDBC 案例代码,可直接用于项目开发,开发者可根据实际业务场景调整分片规则和配置。