Spring Boot 结合 MyBatis-Plus 实现分库分表实战
本文介绍在 Spring Boot 项目中利用 MyBatis-Plus 和 Sharding-JDBC 实现数据库分库分表的方案。涵盖水平分表、垂直分表及水平分库三种场景,详细讲解了配置文件编写、实体类映射、Mapper 接口设计及测试验证流程。同时补充了分片键选择、事务处理及中间件对比等进阶知识,帮助开发者解决单库单表性能瓶颈问题。

本文介绍在 Spring Boot 项目中利用 MyBatis-Plus 和 Sharding-JDBC 实现数据库分库分表的方案。涵盖水平分表、垂直分表及水平分库三种场景,详细讲解了配置文件编写、实体类映射、Mapper 接口设计及测试验证流程。同时补充了分片键选择、事务处理及中间件对比等进阶知识,帮助开发者解决单库单表性能瓶颈问题。

在分布式系统开发中,随着业务规模的扩大,单库单表的架构往往会面临性能瓶颈——数据量激增导致查询缓慢、写入耗时变长,甚至出现表锁阻塞等问题。MyBatis-Plus(简称 MP)作为 MyBatis 的增强工具,不仅简化了 CRUD 操作,还能与分库分表中间件无缝集成,轻松解决数据量大的存储难题。本文将从分库分表的核心概念入手,结合 Spring Boot 实战,通过详细的示例代码讲解 MyBatis-Plus 实现分库分表的完整流程,并补充相关拓展知识点,帮助开发者快速上手。
在讲解实操前,我们先明确分库分表的核心逻辑——本质是'拆分'与'路由':通过将海量数据拆分到多个数据库或数据表中,降低单库单表的数据量,从而提升读写性能;同时通过中间件实现数据的自动路由,让开发者像操作单库单表一样操作拆分后的数据源。
按'数据行'拆分,拆分后每张表/每个库的结构完全相同,数据按规则分布。比如:
适用场景:数据量巨大,且查询多基于拆分字段(如 user_id、时间)。
按'数据列'拆分,拆分后每张表/每个库的结构不同,存储不同维度的字段。比如:
适用场景:表字段过多,或不同字段的查询频率差异大(如核心字段高频查询,扩展字段低频查询)。
核心原则:拆分后尽量避免跨库跨表查询(如联合查询多个拆分表),否则会降低性能。若必须跨查询,需通过中间件或业务逻辑优化。
本文采用'Spring Boot + MyBatis-Plus + Sharding-JDBC'实现分库分表——Sharding-JDBC 是阿里开源的轻量级分库分表中间件,无需额外部署独立服务,可直接集成到应用中,支持所有基于 JDBC 的 ORM 框架(包括 MyBatis-Plus)。
| 技术 | 版本 | 说明 |
|---|---|---|
| Spring Boot | 2.7.10 | 稳定版,适配大多数中间件 |
| MyBatis-Plus | 3.5.3.1 | 支持 Sharding-JDBC 的增强功能 |
| Sharding-JDBC | 4.1.1 | 轻量级分库分表中间件 |
| MySQL | 8.0.x | 关系型数据库(示例用) |
| Lombok | 1.18.24 | 简化实体类代码(可选) |
在 Spring Boot 项目中引入核心依赖,重点是 MyBatis-Plus Starter 和 Sharding-JDBC Starter:
<!-- Spring Boot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- Sharding-JDBC Starter(分库分表核心) -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok(简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
本文将演示 3 个核心场景:水平分表、垂直分表、水平分库。提前创建对应的数据库和表:
创建 SQL 示例(以 MySQL 为例):
-- 1. 水平分表:创建数据库 sharding_db
CREATE DATABASE IF NOT EXISTS sharding_db CHARACTER SET utf8mb4;
USE sharding_db;
-- 水平分表:t_user_0 和 t_user_1(结构相同)
CREATE TABLE IF NOT EXISTS t_user_0 (
id BIGINT NOT NULL COMMENT '用户 ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
age INT NOT NULL COMMENT '年龄',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS t_user_1 LIKE t_user_0;
-- 垂直分表:t_user_base(核心字段)和 t_user_extend(扩展字段)
CREATE TABLE IF NOT EXISTS t_user_base (
id BIGINT NOT NULL COMMENT '用户 ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
phone VARCHAR(20) NOT NULL COMMENT '手机号',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS t_user_extend (
id BIGINT NOT NULL COMMENT '用户 ID(与 t_user_base 关联)',
avatar VARCHAR(255) COMMENT '头像地址',
address VARCHAR(255) COMMENT '详细地址',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 水平分库:创建 2 个数据库 sharding_db_0、sharding_db_1
CREATE DATABASE IF NOT EXISTS sharding_db_0 CHARACTER SET utf8mb4;
CREATE DATABASE IF NOT EXISTS sharding_db_1 CHARACTER SET utf8mb4;
-- 每个库创建 t_order 表(结构相同)
USE sharding_db_0;
CREATE TABLE IF NOT EXISTS t_order (
id BIGINT NOT NULL COMMENT '订单 ID',
user_id BIGINT NOT NULL COMMENT '用户 ID',
order_no VARCHAR(50) NOT NULL COMMENT '订单编号',
amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
USE sharding_db_1;
CREATE TABLE IF NOT EXISTS t_order LIKE sharding_db_0.t_order;
Sharding-JDBC 的核心配置是'数据源配置'和'分片规则配置'——通过配置告诉中间件:数据拆分到哪些库/表?按什么规则路由?下面结合 3 个场景逐一演示。
需求:将用户数据按 user_id 取模拆分到 t_user_0 和 t_user_1 表(user_id%2=0→t_user_0,user_id%2=1→t_user_1),使用 MyBatis-Plus 实现 CRUD 操作。
spring:
# Sharding-JDBC 配置
shardingsphere:
# 数据源配置(水平分表仅 1 个数据源)
datasources:
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root123456
# 分片规则配置
sharding:
# 表分片规则(重点)
tables:
# 逻辑表名(自定义,对应 MP 实体类的表名)
t_user:
# 数据节点:格式为 数据源名称。真实表名,多个表用逗号分隔
actual-data-nodes: ds0.t_user_$->{0..1}
# 表分片策略(按 user_id 取模)
table-strategy:
inline:
# 分片键(即按哪个字段拆分)
sharding-column: id
# 分片表达式(user_id%2 得到 0 或 1,对应 t_user_0 和 t_user_1)
algorithm-expression: t_user_$->{id % 2}
# 主键生成策略(可选,MP 也可配置)
key-generators:
t_user_key:
type: SNOWFLAKE
props:
worker-id: 123
# 打印 SQL(开发环境开启,便于调试)
props:
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
global-config:
db-config:
id-type: ASSIGN_ID # 主键生成策略(雪花算法,与 Sharding-JDBC 一致)
package com.example.sharding.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_user") // 对应 Sharding-JDBC 配置的逻辑表名
public class User {
private Long id; // 分片键(用户 ID)
private String username;
private Integer age;
private LocalDateTime createTime; // 对应数据库的 create_time(下划线转驼峰)
}
MyBatis-Plus 的 BaseMapper 已提供 CRUD 方法,直接继承即可:
package com.example.sharding.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.sharding.entity.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {}
通过测试验证数据是否按规则路由到对应表:
package com.example.sharding.mapper;
import com.example.sharding.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.List;
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
// 测试新增:验证按 id 取模拆分
@Test
public void testInsert() {
// id=1(1%2=1→t_user_1)
User user1 = new User();
user1.setId(1L);
user1.setUsername("张三");
user1.setAge(25);
user1.setCreateTime(LocalDateTime.now());
userMapper.insert(user1);
// id=2(2%2=0→t_user_0)
User user2 = new User();
user2.setId(2L);
user2.setUsername("李四");
user2.setAge(30);
user2.setCreateTime(LocalDateTime.now());
userMapper.insert(user2);
System.out.println("新增成功,可去数据库查看 t_user_0 和 t_user_1 表数据");
}
// 测试查询:验证自动路由到对应表
@Test
public void testSelect() {
// 查询 id=1 的用户(应从 t_user_1 查询)
User user1 = userMapper.selectById(1L);
System.out.println("id=1 的用户:" + user1);
// 查询所有用户(自动聚合 t_user_0 和 t_user_1 的数据)
List<User> userList = userMapper.selectList(null);
System.out.println("所有用户:" + userList);
}
}
需求:将用户数据拆分为核心字段(t_user_base)和扩展字段(t_user_extend),通过 MyBatis-Plus 实现关联查询和分别插入。
垂直分表无需复杂的分片规则,只需配置数据源,MP 通过不同的实体类和 Mapper 对应不同的表:
spring:
shardingsphere:
datasources:
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root123456
# 垂直分表无需表分片规则(直接操作真实表)
props:
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
global-config:
db-config:
id-type: ASSIGN_ID
// UserBase.java(对应 t_user_base 表,核心字段)
package com.example.sharding.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_user_base") // 对应真实表名
public class UserBase {
private Long id; // 关联键
private String username;
private String phone;
private LocalDateTime createTime;
}
// UserExtend.java(对应 t_user_extend 表,扩展字段)
package com.example.sharding.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_user_extend") // 对应真实表名
public class UserExtend {
private Long id; // 与 UserBase 的 id 关联
private String avatar;
private String address;
private LocalDateTime updateTime;
}
package com.example.sharding.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.sharding.entity.UserBase;
import org.springframework.stereotype.Repository;
@Repository
public interface UserBaseMapper extends BaseMapper<UserBase> {}
package com.example.sharding.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.sharding.entity.UserExtend;
import org.springframework.stereotype.Repository;
@Repository
public interface UserExtendMapper extends BaseMapper<UserExtend> {}
垂直分表的关联查询需通过 XML 自定义 SQL(关联 t_user_base 和 t_user_extend),创建 UserMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.sharding.mapper.UserBaseMapper">
<!-- 关联查询用户完整信息(核心字段 + 扩展字段) -->
<select id="selectUserFullInfo" resultType="java.util.Map">
SELECT ub.id, ub.username, ub.phone, ub.create_time, ue.avatar, ue.address, ue.update_time
FROM t_user_base ub
LEFT JOIN t_user_extend ue ON ub.id = ue.id
WHERE ub.id = #{id}
</select>
</mapper>
在 UserBaseMapper 中添加方法:
Map<String, Object> selectUserFullInfo(Long id);
package com.example.sharding.mapper;
import com.example.sharding.entity.UserBase;
import com.example.sharding.entity.UserExtend;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.Map;
@SpringBootTest
public class UserVerticalShardingTest {
@Autowired
private UserBaseMapper userBaseMapper;
@Autowired
private UserExtendMapper userExtendMapper;
// 测试分别插入核心字段和扩展字段
@Test
public void testInsertVertical() {
// 插入核心字段(t_user_base)
UserBase userBase = new UserBase();
userBase.setId(3L);
userBase.setUsername("王五");
userBase.setPhone("13800138000");
userBase.setCreateTime(LocalDateTime.now());
userBaseMapper.insert(userBase);
// 插入扩展字段(t_user_extend)
UserExtend userExtend = new UserExtend();
userExtend.setId(3L); // 与 userBase 的 id 关联
userExtend.setAvatar("https://xxx.com/avatar/3.jpg");
userExtend.setAddress("北京市海淀区");
userExtendMapper.insert(userExtend);
System.out.println("垂直分表插入成功");
}
// 测试关联查询完整信息
@Test
public void testSelectFullInfo() {
Map<String, Object> userInfo = userBaseMapper.selectUserFullInfo(3L);
System.out.println("用户完整信息:" + userInfo);
}
}
需求:将订单数据按 user_id 取模拆分到 sharding_db_0 和 sharding_db_1(user_id%2=0→sharding_db_0,user_id%2=1→sharding_db_1),每个库中的订单表为 t_order。
水平分库需配置多个数据源,并指定数据库分片规则和表分片规则(本例表不分片,仅库分片):
spring:
shardingsphere:
# 多数据源配置(sharding_db_0 和 sharding_db_1)
datasources:
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db_0?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root123456
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db_1?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root123456
# 分片规则配置(库分片 + 表分片)
sharding:
# 数据库分片规则(按 user_id 取模)
default-database-strategy:
inline:
sharding-column: user_id # 库分片键(用户 ID)
algorithm-expression: ds$->{user_id % 2} # 分片表达式(user_id%2=0→ds0,1→ds1)
# 表分片规则(本例表不分片,仅指定真实表名)
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order # 数据节点:ds0.t_order 和 ds1.t_order
# 主键生成策略
key-generators:
t_order_key:
type: SNOWFLAKE
props:
worker-id: 123
props:
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
global-config:
db-config:
id-type: ASSIGN_ID
package com.example.sharding.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("t_order") // 对应真实表名(每个库都有 t_order 表)
public class Order {
private Long id; // 订单 ID
private Long userId; // 库分片键(用户 ID)
private String orderNo;
private BigDecimal amount;
private LocalDateTime createTime;
}
package com.example.sharding.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.sharding.entity.Order;
import org.springframework.stereotype.Repository;
@Repository
public interface OrderMapper extends BaseMapper<Order> {}
package com.example.sharding.mapper;
import com.example.sharding.entity.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@SpringBootTest
public class OrderMapperTest {
@Autowired
private OrderMapper orderMapper;
// 测试新增订单:验证按 userId 分库
@Test
public void testInsertOrder() {
// userId=1(1%2=1→ds1→sharding_db_1 库)
Order order1 = new Order();
order1.setId(1001L);
order1.setUserId(1L);
order1.setOrderNo("ORDER20240501001");
order1.setAmount(new BigDecimal("99.99"));
order1.setCreateTime(LocalDateTime.now());
orderMapper.insert(order1);
// userId=2(2%2=0→ds0→sharding_db_0 库)
Order order2 = new Order();
order2.setId(1002L);
order2.setUserId(2L);
order2.setOrderNo("ORDER20240501002");
order2.setAmount(new BigDecimal("199.99"));
order2.setCreateTime(LocalDateTime.now());
orderMapper.insert(order2);
System.out.println("水平分库新增订单成功,可去 sharding_db_0 和 sharding_db_1 库查看 t_order 表");
}
// 测试查询订单:验证自动路由到对应库
@Test
public void testSelectOrder() {
// 查询 userId=1 的订单(应从 sharding_db_1 库查询)
List<Order> orderList1 = orderMapper.selectList(Wrappers.<Order>lambdaQuery().eq(Order::getUserId, 1L));
System.out.println("userId=1 的订单:" + orderList1);
// 查询 userId=2 的订单(应从 sharding_db_0 库查询)
List<Order> orderList2 = orderMapper.selectList(Wrappers.<Order>lambdaQuery().eq(Order::getUserId, 2L));
System.out.println("userId=2 的订单:" + orderList2);
}
}
分库分表后,单库事务仍可用 Spring 的@Transactional,但跨库事务会失效。解决方案:
| 中间件 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Sharding-JDBC | 轻量级、无独立部署、适配所有 JDBC 框架、性能好 | 不支持读写分离自动切换(需额外配置)、动态扩容复杂 | 中小型分布式系统、对性能要求高的场景 |
| MyCat | 支持读写分离、动态扩容、功能强大 | 需独立部署、性能略低于 Sharding-JDBC、配置复杂 | 大型分布式系统、需要丰富分库分表功能的场景 |
| Sharding-Proxy | 支持多语言、动态扩容、统一管理分片规则 | 需独立部署、性能有损耗(代理层) | 多语言开发、需要统一管理分片规则的场景 |
本文通过 Spring Boot + MyBatis-Plus + Sharding-JDBC 实现了水平分表、垂直分表、水平分库三个核心场景的实战,详细讲解了配置流程、示例代码和测试验证。分库分表的核心是'合理拆分'与'自动路由',开发者需根据业务场景选择合适的拆分方式和分片键,同时注意事务处理、热点数据等问题。
MyBatis-Plus 的增强功能(如 BaseMapper、条件构造器)与 Sharding-JDBC 无缝集成,大大降低了分库分表的开发成本。在实际项目中,还需结合业务需求选择合适的中间件,并做好性能监控和动态扩容方案,确保系统稳定运行。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online