基于mybatis-plus动态数据源实现mysql集群读写分离和从库负载均衡教程(详细案例)

基于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 动态数据源插件 实现了:

  1. 读写分离 :写操作默认走主库,读操作通过 @DS 注解走从库;
  2. 从库负载均衡:从库集群(slave_group)按轮询策略分发读请求;
  3. 灵活路由 :支持指定单个从库(如 @DS("slave1"))或强制走主库(如 @DS("master"))。

该方案无需额外中间件,代码侵入性低,适合中小型 Spring Boot 项目快速落地读写分离,且后续可通过自定义负载均衡策略(如权重)进一步优化。

相关推荐
万邦科技Lafite4 小时前
实战演练:通过API获取商品详情并展示
大数据·数据库·python·开放api接口
黄焖鸡能干四碗4 小时前
智慧教育,智慧校园,智慧安防学校建设解决方案(PPT+WORD)
java·大数据·开发语言·数据库·人工智能
敲上瘾4 小时前
Docker 存储卷(Volume)核心概念、类型与操作指南
linux·服务器·数据库·docker·容器·架构
DemonAvenger4 小时前
MySQL内存优化:缓冲池与查询缓存调优实战指南
数据库·mysql·性能优化
RationalDysaniaer4 小时前
了解etcd
数据库·etcd
正在走向自律5 小时前
国产时序数据库选型指南-从大数据视角看透的价值
大数据·数据库·清华大学·时序数据库·iotdb·国产数据库
愿时间能学会宽恕5 小时前
SpringBoot后端开发常用工具详细介绍——SpringSecurity认证用户保证安全
spring boot·后端·安全
Pocker_Spades_A5 小时前
Python快速入门专业版(十五):数据类型实战:用户信息录入程序(整合变量、输入与类型转换)
数据库·python