「Spring Boot + MyBatis-Plus + MySQL 一主两从」读写分离实战教程
🚀 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)
<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 多数据源配置
spring:datasource:dynamic:primary: masterstrict: falsedatasource:master:url: jdbc:mysql://mysql-master:3307/testdb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: rootslave1:url: jdbc:mysql://mysql-slave1:3308/testdb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: rootslave2:url: jdbc:mysql://mysql-slave2:3309/testdb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: rootmybatis-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语句 |
点击这里查看高效管理Hosts文件的终极工具
⚙️ MyBatis-Plus 分页配置(可选)
📁 config/DataSourceAspect.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
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 {@Beanpublic 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 | 页码溢出(如:页码 > 总页数)是否回到首页 |
🛠 使用示例代码
Page<User> page = new Page<>(1, 10); // 查询第1页,每页10条
Page<User> result = userMapper.selectPage(page, null);
MyBatis-Plus 会自动生成如下 SQL:
SELECT * FROM user LIMIT 0, 10;
📚 实体类 + Mapper + Service 示例
📁 domain/User.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
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
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
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
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 {@Autowiredprivate 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):
{"name": "张三","age": 28
}
curl 示例:
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 表结构
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 / 微服务实战案例 🚀