🚀 Spring Boot + MyBatis-Plus + MySQL 一主两从多数据源实战(99% 场景覆盖)
在企业级开发中,数据库的读写分离 几乎已成标配。本文基于 Spring Boot 3.3.12 + MyBatis-Plus + MySQL 构建一个一主两从动态多数据源读写分离项目 ,覆盖市面上 99% 的常见场景。
🔧 技术栈:Spring Boot 3.3.12 + MyBatis-Plus 3.5.5 + dynamic-datasource 4.2.0 + MySQL + JDK17
🐳 MySQL 主从复制 Docker Compose 配置
点击这里查看 MySQL 主从复制 Docker Compose 配置
🧭 项目结构总览
springboot-mybatisplus-dynamic-ds/
├── pom.xml
├── README.md
├── src/
│ ├── main/
│ │ ├── java/com/example/demo/
│ │ │ ├── config/ 🛠️ 数据源、分页、全局配置
│ │ │ ├── controller/ 🎮 控制器
│ │ │ ├── domain/ 📦 实体类
│ │ │ ├── mapper/ 🧭 Mapper 接口
│ │ │ ├── service/ 💼 业务接口
│ │ │ ├── service/impl/ 💡 业务实现
│ │ │ └── SpringbootMybatisApp.java # 🚀 启动类
│ ├── resources/
│ │ ├── application.yml 📘 配置文件
│ │ └── mapper/ 🧾 Mapper XML 文件
└── ...
📦 项目依赖(pom.xml)
xml
<properties>
<java.version>17</java.version>
<spring.boot.version>3.3.12</spring.boot.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<dynamic.datasource.version>4.2.0</dynamic.datasource.version>
</properties>
<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-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- 多数据源插件 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${dynamic.datasource.version}</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>
🛠️ application.yml 多数据源配置
yaml
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
url: jdbc:mysql://mysql-master:3307/testdb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
slave1:
url: jdbc:mysql://mysql-slave1:3308/testdb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
slave2:
url: jdbc:mysql://mysql-slave2:3309/testdb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
✅ 解释
配置项 | 含义 |
---|---|
primary: master |
默认使用主库(写库) |
strict: false |
未标注数据源时不会报错,自动使用主库 |
datasource.master |
主库连接信息(写) |
datasource.slave1 |
从库1连接信息(读) |
datasource.slave2 |
从库2连接信息(读) |
log-impl: StdOutImpl |
控制台打印SQL语句 |
⚙️ MyBatis-Plus 分页配置(可选)
📁 config/DataSourceAspect.java
java
package com.example.demo.aspect;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAspect {
@Before("execution(* com.example.demo..*.*(..))")
public void before(JoinPoint point) {
String ds = DynamicDataSourceContextHolder.peek();
System.out.println("🔥 当前使用数据源:" + (ds != null ? ds : "master(默认)"));
}
}
这段切面代码的作用是:在调用 com.example.demo 包下任意方法前,打印当前使用的数据源(若未指定则默认使用 master)。
📁 config/MybatisPlusConfig.java
java
package com.example.demo.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
下面是对你这段 MybatisPlusConfig
配置类的详细解释:
✅ MybatisPlusConfig.java
分页插件配置说明表
行号 | 代码片段 | 说明 |
---|---|---|
1 | @Configuration |
声明这是一个 Spring 配置类,会被自动加载到 Spring 容器。 |
2 | public class MybatisPlusConfig |
自定义配置类,用于配置 MyBatis-Plus 的功能。 |
4 | @Bean |
将方法返回值交给 Spring 容器管理,作为一个 Bean 使用。 |
5 | public MybatisPlusInterceptor mybatisPlusInterceptor() |
定义并注册一个 MybatisPlusInterceptor 拦截器 Bean。 |
6 | MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); |
创建新的 MyBatis-Plus 插件拦截器(新版3.4+)。 |
8 | interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); |
添加分页插件,指定数据库类型为 MySQL。 它会拦截分页查询并自动添加 LIMIT 语句。 |
9 | return interceptor; |
返回配置完成的分页拦截器 Bean。 |
📌 分页插件功能补充说明
配置项 | 类型 | 示例值 | 含义 |
---|---|---|---|
DbType |
枚举 | DbType.MYSQL |
指定分页插件支持的数据库类型 |
setMaxLimit |
Long | 500L |
设置分页单页最大返回记录数(防止恶意超大页) |
setOverflow |
boolean | true |
页码溢出(如:页码 > 总页数)是否回到首页 |
🛠 使用示例代码
java
Page<User> page = new Page<>(1, 10); // 查询第1页,每页10条
Page<User> result = userMapper.selectPage(page, null);
MyBatis-Plus 会自动生成如下 SQL:
sql
SELECT * FROM user LIMIT 0, 10;
📚 实体类 + Mapper + Service 示例
📁 domain/User.java
java
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
/**
* 用户实体类
*/
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
}
📁 mapper/UserMapper.java
java
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承BaseMapper后,提供了丰富的 CRUD 方法
}
📁 service/UserService.java
java
package com.example.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.entity.User;
import java.util.List;
/**
* 用户业务接口
* <p>
* 继承 MyBatis-Plus 的 IService<T>,自动拥有:
* - CRUD 方法:save、updateById、removeById、getById 等
* - 批量操作、分页查询、条件构造器支持
*/
public interface UserService extends IService<User> {
/**
* 从从库 slave1 获取用户列表(示例切库)
*
* @return 用户列表
*/
List<User> listFromSlave1();
/**
* 从从库 slave2 获取用户列表(示例切库)
*
* @return 用户列表
*/
List<User> listFromSlave2();
}
📁 service/impl/UserServiceImpl.java
java
package com.example.demo.service.impl;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 用户业务接口实现类
* <p>
* 默认所有数据库操作走 master 主库,可通过 @DS("slave1") / @DS("slave2") 注解指定从库
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
/**
* 从从库 slave1 查询所有用户数据
* @return 用户列表
*/
@Override
@DS("slave1")
public List<User> listFromSlave1() {
return this.list();
}
/**
* 从从库 slave2 查询所有用户数据
* @return 用户列表
*/
@Override
@DS("slave2")
public List<User> listFromSlave2() {
return this.list();
}
}
🎮 Controller 示例
📁 controller/UserController.java
java
package com.example.demo.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 添加用户,不配置@DS,默认master
*/
@PostMapping("/add")
public boolean add(@RequestBody User user) {
return userService.save(user);
}
/**
* 在 slave1数据库 查询所有用户
*/
@GetMapping("/slave1")
public List<User> fromSlave1() {
return userService.listFromSlave1();
}
/**
* 在 slave2数据库 查询所有用户
*/
@GetMapping("/slave2")
public List<User> fromSlave2() {
return userService.listFromSlave2();
}
/**
* 分页查询,不配置@DS,默认master
*/
@GetMapping("/page")
public IPage<User> getPagedUsers(@RequestParam int page, @RequestParam int size) {
return userService.page(Page.of(page, size));
}
}
🌐 1. POST /users ------ 新增用户(主库)
请求方式: POST
URL:
http://localhost:8080/api/user/add
请求体(JSON):
json
{
"name": "张三",
"age": 28
}
curl 示例:
bash
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"张三","age":28}'

🌐 2. GET /users/slave1 ------ 从从库1获取用户列表
请求方式: GET
URL:
http://localhost:8080/api/user/slave1

🌐 3. GET /users/slave2 ------ 从从库2获取用户列表
请求方式: GET
URL:
http://localhost:8080/api/user/slave2

🌐 4. GET /users/page?page=1&size=5 ------ 分页查询用户(主库)
请求方式: GET
URL:
http://localhost:8080/api/user/page?page=1&size=5

📖 MyBatis-Plus 常用方法全解析
方法 | 说明 | 示例 |
---|---|---|
save(entity) |
插入一条记录 | userService.save(user); |
saveBatch(Collection) |
批量插入 | userService.saveBatch(usersList); |
getById(id) |
根据ID查询单条记录 | userService.getById(1L); |
list() |
查询所有记录 | userService.list(); |
list(QueryWrapper) |
条件查询 | userService.list(new QueryWrapper<User>().eq("age", 20)); |
page(IPage) |
分页查询 | userService.page(new Page<>(1, 10)); |
updateById(entity) |
根据ID更新记录 | userService.updateById(user); |
removeById(id) |
根据ID删除记录(逻辑删除) | userService.removeById(1L); |
remove(QueryWrapper) |
条件删除 | userService.remove(new QueryWrapper<User>().lt("age", 18)); |
selectCount(QueryWrapper) |
统计满足条件的记录数 | userMapper.selectCount(new QueryWrapper<User>().eq("age", 20)); |
乐观锁(@Version) | 自动维护版本号,避免并发冲突 | 配合 updateById() 使用 |
逻辑删除(@TableLogic) | 自动过滤已逻辑删除数据 | 查询默认不返回已删除数据 |
🧪 示例 SQL 表结构
sql
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64),
age INT
);
分别在 master、slave1、slave2 初始化相同表结构,确保数据一致性。
📌 项目亮点总结
✅ 支持多数据源自动切换
✅ 支持 MyBatis-Plus 分页与条件构造器
✅ 使用注解即可切库,简洁无侵入
✅ 分层设计,业务清晰可拓展
✅ 适配 JDK17 + Spring Boot 3.3.x
❤️ 写在最后
👏 感谢阅读!
本项目为真实业务场景整理,涵盖读写分离 + 分页 + MyBatis-Plus CRUD 全面使用。建议搭配实际需求灵活扩展,如添加 Redis 缓存、权限框架等。
如果你觉得这篇文章对你有帮助,欢迎一键三连:点赞 👍、收藏 ⭐、评论 💬!
也欢迎关注我,第一时间获取更多 Spring Boot / 微服务实战案例 🚀