Spring Boot + MySQL读写分离实现方案
- 一、MySQL主从复制搭建(一主一从)
-
- [1. 主库配置](#1. 主库配置)
- [2. 从库配置](#2. 从库配置)
- [二、Spring Boot读写分离实现(推荐Dynamic-Datasource)](#二、Spring Boot读写分离实现(推荐Dynamic-Datasource))
-
- [1. 添加Maven依赖(pom.xml)](#1. 添加Maven依赖(pom.xml))
- [2. 配置文件(application.yml)](#2. 配置文件(application.yml))
- [3. 代码实现](#3. 代码实现)
-
- [a. 定义数据源枚举](#a. 定义数据源枚举)
- [b. 数据源上下文管理器](#b. 数据源上下文管理器)
- [c. 使用AOP标注读写操作](#c. 使用AOP标注读写操作)
- [d. AOP切面实现](#d. AOP切面实现)
- [e. 在Service层使用](#e. 在Service层使用)
- 三、Redis和MQ整合方案分析
-
- [✅ 优点:](#✅ 优点:)
- [⚠️ 问题与建议:](#⚠️ 问题与建议:)
- [🛠️ 优化:](#🛠️ 优化:)
- 四、总结
一、MySQL主从复制搭建(一主一从)
1. 主库配置
bash
# 1. 修改主库配置文件 /etc/my.cnf
[mysqld]
server-id=1 # 唯一ID
log-bin=mysql-bin # 启用二进制日志
binlog-format=mixed # 二进制日志格式
重启MySQL后执行:
sql
# 2. 创建复制账号
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
# 3. 查看二进制日志坐标
SHOW MASTER STATUS;
# 记录File和Position,例如:File 'mysql-bin.000001', Position 154
2. 从库配置
bash
# 1. 修改从库配置文件 /etc/my.cnf
[mysqld]
server-id=2 # 必须与主库不同
relay-log=mysql-relay-bin # 中继日志
log-slave-updates=1 # 使从库也能作为其他从库的主库
重启MySQL后执行:
sql
# 2. 配置主从连接
CHANGE MASTER TO
MASTER_HOST='主库IP',
MASTER_USER='repl',
MASTER_PASSWORD='repl_password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=154;
# 3. 启动复制
START SLAVE;
# 4. 验证
SHOW SLAVE STATUS\G
# 确认Slave_IO_Running和Slave_SQL_Running都是Yes
💡 小贴士:主从复制需要确保网络通畅,主库防火墙开放3306端口,从库能访问主库的3306端口。
二、Spring Boot读写分离实现(推荐Dynamic-Datasource)
1. 添加Maven依赖(pom.xml)
xml
<dependencies>
<!-- Spring Boot Starter -->
<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.3.2</version>
</dependency>
<!-- Dynamic-Datasource(核心) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 连接池(可选) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
2. 配置文件(application.yml)
yaml
spring:
datasource:
dynamic:
primary: master # 默认数据源
datasource:
master:
url: jdbc:mysql://主库IP:3306/db_name?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: master_password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
slave:
url: jdbc:mysql://从库IP:3306/db_name?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: slave_password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 15
minimum-idle: 8
strategy:
# 读写分离策略:读操作走从库,写操作走主库
# 可选:round_robin(轮询)、random(随机)等
datasource:
master: slave
3. 代码实现
a. 定义数据源枚举
java
public enum DataSourceType {
MASTER, // 主库:写操作
SLAVE // 从库:读操作
}
b. 数据源上下文管理器
java
public class DataSourceContextHolder {
private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(DataSourceType dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
public static DataSourceType getDataSourceType() {
return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER : CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
c. 使用AOP标注读写操作
java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
DataSourceType value() default DataSourceType.MASTER;
}
d. AOP切面实现
java
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void before(JoinPoint point, DataSource dataSource) {
DataSourceType type = dataSource.value();
DataSourceContextHolder.setDataSourceType(type);
}
@After("@annotation(dataSource)")
public void after(JoinPoint point, DataSource dataSource) {
DataSourceContextHolder.clearDataSourceType();
}
}
e. 在Service层使用
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 写操作:默认走主库(也可以显式指定)
@DataSource(DataSourceType.MASTER)
public void createOrder(Order order) {
orderMapper.insert(order);
}
// 读操作:默认走从库
@DataSource(DataSourceType.SLAVE)
public Order getOrderById(Long id) {
return orderMapper.selectById(id);
}
}
三、Redis和MQ整合方案分析
项目引入 redis和mq ,针对【热门数据 】的查询可以从redis查询,代码层面往MySQL主库执行增删改操作后往mq发送一条消息,然后由消费者将增删改的数据同步到redis中,当然这样的方案只适合少量的增删改操作,不适合大批量的增删改。
✅ 优点:
- 保证了数据一致性(先写库,再更新缓存)
- 适合热点数据缓存
- 降低数据库压力
⚠️ 问题与建议:
-
不适合大批量操作:大批量增删改会导致MQ消息量过大,处理延迟高。建议:
- 对于大批量操作,使用批量同步(如Redis的pipeline)而不是MQ。
- 或者使用定时任务定期同步热点数据。
-
消息可靠性:需要考虑MQ消息丢失、重复消费问题:
- 使用RocketMQ/RabbitMQ的事务消息。
- 添加消息重试机制。
- 消费者 添加
幂等性处理。
-
更适合的场景:
- 用户信息、商品详情等高频查询数据。
- 但
不适用于订单状态、交易流水等高一致性要求的数据。
🛠️ 优化:
java
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
@DataSource(DataSourceType.MASTER)
@Transactional
public Order createOrder(Order order) {
// 1. 写数据库
Order savedOrder = orderMapper.insert(order);
// 2. 发送MQ消息(只针对需要缓存的数据)
if (isCacheableData(order)) {
rabbitTemplate.convertAndSend("order-cache-topic",
new OrderCacheMessage(savedOrder.getId(), "CREATE"));
}
return savedOrder;
}
}
// 消费者处理
@Component
public class OrderCacheConsumer {
@RabbitListener(queues = "order-cache-queue")
public void process(OrderCacheMessage message) {
if ("CREATE".equals(message.getAction())) {
Order order = orderMapper.selectById(message.getOrderId());
redisTemplate.opsForValue().set("order:" + message.getOrderId(), order);
}
// 处理其他操作...
}
}
四、总结
-
读写分离:使用Dynamic-Datasource,配置简单,社区支持好,比Sharding-JDBC更轻量
-
Redis+MQ方案:
- 适合
热点数据的缓存更新。 不适用于大批量操作。- 建议对需要缓存的数据做分类,只对高频查询的数据使用MQ同步。
- 适合
-
建议:
- 在查询方法上使用
@DataSource(DataSourceType.SLAVE)显式指定,避免默认路由错误。 - 对于复杂查询,可以考虑在MyBatis中使用
@Select指定数据源。 - 监控主从延迟,避免从库数据不一致。
- 在查询方法上使用
🌟 小技巧 :在开发阶段,可以在配置中添加
dynamic.datasource.log=true,这样能打印出实际使用的数据源,方便排查问题。