基于mysql集群&mybatis-plus动态数据源实现数据库读写分离和从库负载均衡教程(详细案例)
本案例基于 Spring Boot 2.7.x + MyBatis-Plus 3.5.x + MySQL 8.0 实现,包含完整的项目搭建、配置、代码编写和测试验证,适合中小型项目快速落地读写分离。
一、环境准备
1. 数据库环境(已搭建的主从集群)
角色 | IP地址 | 数据库账号 | 权限说明 |
---|---|---|---|
主库 | 192.168.1.100 | root/Root@123456 | 读写权限(处理写请求) |
从库1 | 192.168.1.101 | read_user/Read@123456 | 只读权限(仅SELECT,处理读请求) |
从库2 | 192.168.1.102 | read_user/Read@123456 | 只读权限(仅SELECT,处理读请求) |
2. 项目依赖(pom.xml)
创建 Spring Boot 项目,在 pom.xml
中引入核心依赖(动态数据源、MyBatis-Plus、MySQL 驱动等):
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version> <!-- 稳定版 -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>mysql-read-write-split</artifactId>
<version>1.0-SNAPSHOT</version>
<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.4.1</version>
</dependency>
<!-- 动态数据源插件(实现读写分离核心) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<!-- MySQL 驱动(适配 MySQL 8.0) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok(简化实体类代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot 测试(用于单元测试) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
二、核心配置(application.yml)
在 src/main/resources/application.yml
中配置 多数据源、负载均衡策略、MyBatis-Plus 等核心参数:
yaml
# 服务端口
server:
port: 8080
# 数据库配置(动态数据源)
spring:
datasource:
dynamic:
primary: master # 默认数据源(写请求默认走主库)
strict: false # 非严格模式:找不到指定数据源时,自动使用primary数据源
load-balance: round_robin # 从库负载均衡策略:round_robin(轮询)、random(随机)
health: true # 开启数据源健康检测(故障从库自动剔除)
health-timeout: 3000 # 健康检测超时时间(毫秒)
# 多数据源列表
datasource:
# 主库数据源(处理写请求)
master:
url: jdbc:mysql://192.168.1.100:3306/test?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: root
password: Root@123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 从库1数据源(处理读请求)
slave1:
url: jdbc:mysql://192.168.1.101:3306/test?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: read_user
password: Read@123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 从库2数据源(处理读请求)
slave2:
url: jdbc:mysql://192.168.1.202:3306/test?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username: read_user
password: Read@123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据源分组(将从库归为一组,实现负载均衡)
datasource-group:
slave_group: slave1,slave2 # slave_group组包含slave1和slave2
# MyBatis-Plus 配置
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml # Mapper.xml文件路径
type-aliases-package: com.example.entity # 实体类包路径
configuration:
map-underscore-to-camel-case: true # 开启下划线转驼峰(如user_name -> userName)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(方便测试验证)
三、代码编写(分层实现)
按 实体类 → Mapper → Service → Controller 分层编写代码,核心通过 @DS
注解指定数据源。
1. 实体类(Entity)
对应数据库 test
库的 user
表(提前在主库创建表):
java
package com.example.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体类(对应test.user表)
*/
@Data
@TableName("user") // 关联数据库表名
public class User {
@TableId(type = IdType.AUTO) // 主键自增
private Long id; // 用户ID
private String username; // 用户名
private String password; // 密码(实际项目需加密存储)
private Integer age; // 年龄
private LocalDateTime createTime; // 创建时间(默认当前时间)
}
2. 数据库表创建(主库执行)
在主库 test
库中创建 user
表(从库会自动同步):
sql
CREATE TABLE IF NOT EXISTS `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`age` int DEFAULT NULL COMMENT '年龄',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
3. Mapper 接口(数据访问层)
继承 MyBatis-Plus 的 BaseMapper
,无需手动写 SQL(简单CRUD):
java
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* User Mapper 接口(MyBatis-Plus自动实现CRUD)
*/
@Mapper // 标记为MyBatis Mapper接口
public interface UserMapper extends BaseMapper<User> {
// 无需额外方法,BaseMapper已提供:insert、deleteById、updateById、selectById、selectList等
}
4. Service 层(业务逻辑层)
核心层:通过 @DS
注解指定数据源,实现读写分离与负载均衡。
4.1 Service 接口
java
package com.example.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.User;
import java.util.List;
/**
* User Service 接口
*/
public interface UserService extends IService<User> {
// 写操作:新增用户(走主库)
boolean addUser(User user);
// 读操作:按ID查询用户(走从库集群,负载均衡)
User getUserById(Long id);
// 读操作:查询所有用户(走从库1,指定单个从库)
List<User> listAllUserFromSlave1();
// 读操作:按年龄查询用户(走从库2,指定单个从库)
List<User> listUserByAge(Integer age);
}
4.2 Service 实现类(核心:@DS 注解使用)
java
package com.example.service.impl;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.service.UserService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* User Service 实现类
* 关键:@DS注解指定数据源,未指定则默认走primary(master)
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
/**
* 写操作:新增用户
* 未加@DS注解 → 默认走primary(master主库),符合"写请求走主库"原则
*/
@Override
public boolean addUser(User user) {
return save(user); // MyBatis-Plus提供的save方法(INSERT操作)
}
/**
* 读操作:按ID查询用户
* @DS("slave_group") → 走从库集群(slave1和slave2),按轮询策略负载均衡
*/
@Override
@DS("slave_group")
public User getUserById(Long id) {
return getById(id); // MyBatis-Plus提供的getById方法(SELECT操作)
}
/**
* 读操作:查询所有用户
* @DS("slave1") → 强制走slave1从库
*/
@Override
@DS("slave1")
public List<User> listAllUserFromSlave1() {
return list(); // MyBatis-Plus提供的list方法(SELECT * FROM user)
}
/**
* 读操作:按年龄查询用户
* @DS("slave2") → 强制走slave2从库
* 自定义SQL(需在Mapper.xml中编写)
*/
@Override
@DS("slave2")
public List<User> listUserByAge(Integer age) {
// 调用Mapper自定义方法(参数用Map传递,或用@Param注解)
return baseMapper.selectListByAge(age);
}
}
5. Mapper 自定义 SQL(可选,复杂查询)
若需要复杂查询(如按条件筛选),在 src/main/resources/mapper/UserMapper.xml
中编写 SQL:
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.mapper.UserMapper">
<!-- 自定义查询:按年龄查询用户 -->
<select id="selectListByAge" resultType="com.example.entity.User">
SELECT id, username, password, age, create_time AS createTime
FROM user
WHERE age = #{age}
</select>
</mapper>
同时在 UserMapper
接口中添加对应方法:
java
// UserMapper.java 中新增方法
List<User> selectListByAge(@Param("age") Integer age);
6. Controller 层(接口层,用于测试)
编写 REST 接口,通过 Postman 或浏览器测试读写分离效果:
java
package com.example.controller;
import com.example.entity.User;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* User 控制器(测试读写分离接口)
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 1. 写操作:新增用户(走主库)
* 请求方式:POST
* 请求体:{"username":"test1","password":"123456","age":20}
*/
@PostMapping("/add")
public String addUser(@RequestBody User user) {
boolean success = userService.addUser(user);
return success ? "新增用户成功(走主库)" : "新增用户失败";
}
/**
* 2. 读操作:按ID查询用户(走从库集群,负载均衡)
* 请求方式:GET
* 示例:http://localhost:8080/user/get/1
*/
@GetMapping("/get/{id}")
public User getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return user;
}
/**
* 3. 读操作:查询所有用户(走slave1从库)
* 请求方式:GET
* 示例:http://localhost:8080/user/list/slave1
*/
@GetMapping("/list/slave1")
public List<User> listAllUserFromSlave1() {
return userService.listAllUserFromSlave1();
}
/**
* 4. 读操作:按年龄查询用户(走slave2从库)
* 请求方式:GET
* 示例:http://localhost:8080/user/list/age?age=20
*/
@GetMapping("/list/age")
public List<User> listUserByAge(@RequestParam Integer age) {
return userService.listUserByAge(age);
}
}
四、项目启动类
添加 @MapperScan
注解,扫描 Mapper 接口:
java
package com.example;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目启动类
*/
@SpringBootApplication
@MapperScan("com.example.mapper") // 扫描Mapper接口所在包
public class MysqlReadWriteSplitApplication {
public static void main(String[] args) {
SpringApplication.run(MysqlReadWriteSplitApplication.class, args);
}
}
五、测试验证(核心步骤)
启动项目后,通过 SQL日志 和 数据库数据 验证读写分离与负载均衡效果。
1. 验证"写操作走主库"
步骤1:调用新增用户接口
用 Postman 发送 POST 请求:
-
URL:
http://localhost:8080/user/add
-
请求体:
json{ "username": "test_user1", "password": "123456", "age": 25 }
步骤2:查看 SQL 日志
控制台会打印新增用户的 SQL,且数据源为 master
:
JDBC Connection [com.zaxxer.hikari.HikariProxyConnection@xxxx] will be used by dynamic datasource
==> Preparing: INSERT INTO user ( username, password, age, create_time ) VALUES ( ?, ?, ?, ? )
==> Parameters: test_user1(String), 123456(String), 25(Integer), 2025-09-10T10:00:00(LocalDateTime)
<== Updates: 1
步骤3:验证主从数据同步
- 主库查询:
SELECT * FROM test.user;
→ 能看到test_user1
; - 从库1/2查询:
SELECT * FROM test.user;
→ 也能看到test_user1
(主从同步生效)。
2. 验证"读操作走从库集群(负载均衡)"
步骤1:多次调用"按ID查询"接口
用浏览器或 Postman 多次访问:http://localhost:8080/user/get/1
(假设新增用户的ID为1)。
步骤2:查看 SQL 日志(轮询效果)
第一次请求:数据源为 slave1
:
JDBC Connection [com.zaxxer.hikari.HikariProxyConnection@xxxx] will be used by dynamic datasource : slave1
==> Preparing: SELECT id,username,password,age,create_time AS createTime FROM user WHERE id=?
==> Parameters: 1(Long)
<== Total: 1
第二次请求:数据源自动切换为 slave2
(轮询策略生效):
JDBC Connection [com.zaxxer.hikari.HikariProxyConnection@xxxx] will be used by dynamic datasource : slave2
==> Preparing: SELECT id,username,password,age,create_time AS createTime FROM user WHERE id=?
==> Parameters: 1(Long)
<== Total: 1
第三次请求:又切换回 slave1
,以此循环,证明从库负载均衡生效。
3. 验证"指定从库读取"
步骤1:调用"查询所有用户(slave1)"接口
访问:http://localhost:8080/user/list/slave1
,日志显示数据源为 slave1
:
JDBC Connection [com.zaxxer.hikari.HikariProxyConnection@xxxx] will be used by dynamic datasource : slave1
==> Preparing: SELECT id,username,password,age,create_time AS createTime FROM user
==> Parameters:
<== Total: 1
步骤2:调用"按年龄查询(slave2)"接口
访问:http://localhost:8080/user/list/age?age=25
,日志显示数据源为 slave2
:
JDBC Connection [com.zaxxer.hikari.HikariProxyConnection@xxxx] will be used by dynamic datasource : slave2
==> Preparing: SELECT id, username, password, age, create_time AS createTime FROM user WHERE age = ?
==> Parameters: 25(Integer)
<== Total: 1
六、常见问题处理
1. 从库连接失败(日志显示"Access denied")
-
原因:从库
read_user
账号密码错误,或未授予SELECT
权限。 -
解决:在从库执行授权语句:
sql-- 从库执行:授予read_user只读权限 GRANT SELECT ON test.* TO 'read_user'@'%' IDENTIFIED WITH mysql_native_password BY 'Read@123456'; FLUSH PRIVILEGES;
2. 负载均衡不生效(始终走同一个从库)
- 原因:
application.yml
中未配置datasource-group
,或@DS
注解指定的是单个从库(如@DS("slave1")
)。 - 解决:确保
datasource-group
配置正确,且读操作注解为@DS("slave_group")
。
3. 主从延迟导致读不到新数据
-
原因:主库写入后,数据同步到从库有延迟(如1秒),立即读从库会看到旧数据。
-
解决:核心读请求(如"刚新增完用户就查询")强制走主库,添加
@DS("master")
注解:java@Override @DS("master") // 强制走主库,避免主从延迟 public User getNewUserById(Long id) { return getById(id); }
七、总结
本案例通过 MyBatis-Plus 动态数据源插件 实现了:
- 读写分离 :写操作默认走主库,读操作通过
@DS
注解走从库; - 从库负载均衡:从库集群(slave_group)按轮询策略分发读请求;
- 灵活路由 :支持指定单个从库(如
@DS("slave1")
)或强制走主库(如@DS("master")
)。
该方案无需额外中间件,代码侵入性低,适合中小型 Spring Boot 项目快速落地读写分离,且后续可通过自定义负载均衡策略(如权重)进一步优化。