# AWS JDBC Wrapper读写分离配置实战:Spring Boot + MyBatis Plus完整解决方案
## 前言
在微服务架构中,数据库读写分离是提升系统性能的重要手段。本文将详细介绍如何在Spring Boot项目中使用AWS JDBC Wrapper实现自动读写分离,重点解决MyBatis Plus框架下的配置难点,并对比Spring JPA的差异。
**核心结论**:AWS JDBC Wrapper需要连接的`readOnly`状态来判断路由,MyBatis Plus需要手动添加`@Transactional(readOnly = true)`,而Spring JPA会自动处理。
## 一、AWS JDBC Wrapper简介
### 1.1 什么是AWS JDBC Wrapper
AWS JDBC Wrapper是Amazon提供的数据库连接增强工具,支持:
- 自动故障转移
- 读写分离
- 连接池管理
- 性能监控
### 1.2 读写分离原理
```mermaid
graph TD
A[应用程序] --> B[AWS JDBC Wrapper]
B --> C{检查Connection.readOnly}
C -->|true| D[Aurora Reader Endpoint]
C -->|false| E[Aurora Writer Endpoint]
D --> F[只读副本]
E --> G[主库]
```
**关键机制**:AWS JDBC Wrapper通过检测JDBC连接的`readOnly`属性来决定路由目标。
## 二、基础配置
### 2.1 Maven依赖
```xml
<dependency>
<groupId>software.amazon.jdbc</groupId>
<artifactId>aws-advanced-jdbc-wrapper</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
```
### 2.2 数据源配置
```yaml
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: software.amazon.jdbc.Driver
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
url: jdbc:aws-wrapper:mysql://${AURORA_CLUSTER_ENDPOINT}:3306/${DATABASE_NAME}?wrapperPlugins=readWriteSplitting,failover&characterEncoding=utf-8&wrapperLogLevel=FINEST&useSSL=true&requireSSL=true
```
**重要参数说明**:
- `wrapperPlugins=readWriteSplitting,failover`:启用读写分离和故障转移
- `wrapperLogLevel=FINEST`:启用详细日志,便于调试
### 2.3 日志配置
```xml
<!-- logback.xml -->
<configuration>
<!-- AWS JDBC Wrapper日志 -->
<logger name="software.amazon.jdbc" level="TRACE"/>
<logger name="software.amazon.jdbc.plugin.readwritesplitting" level="TRACE"/>
<logger name="software.amazon.jdbc.plugin.failover" level="TRACE"/>
<!-- HikariCP连接池日志 -->
<logger name="com.zaxxer.hikari" level="DEBUG"/>
</configuration>
```
## 三、核心问题:MyBatis Plus的读写分离挑战
### 3.1 问题现象
**预期**:查询操作自动路由到只读副本
**实际**:所有操作都路由到主库
**关键日志**:
```
TRACE ReadWriteSplittingPlugin - Writer connection set to 'cluster-endpoint:3306'
```
### 3.2 根本原因分析
**Spring JPA vs MyBatis Plus的差异**:
| 框架 | 事务配置 | readOnly设置 | 读写分离效果 |
|------|----------|--------------|-------------|
| Spring JPA | Repository方法自动添加`@Transactional(readOnly=true)` | ✅ 自动 | ✅ 有效 |
| MyBatis Plus | ServiceImpl无自动事务配置 | ❌ 手动 | ❌ 无效 |
**技术原理**:
1. AWS JDBC Wrapper依赖`Connection.setReadOnly(true)`来判断路由
2. Spring事务管理器负责设置连接的readOnly状态
3. 只有在`@Transactional(readOnly=true)`时,Spring才会调用`connection.setReadOnly(true)`
## 四、解决方案
### 4.1 方案一:Service层添加只读事务(推荐)
```java
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
/**
* 查询方法 - 走读库
*/
@Override
@Transactional(readOnly = true)
public List<User> list(QueryWrapper<User> queryWrapper) {
return super.list(queryWrapper);
}
/**
* 分页查询 - 走读库
*/
@Override
@Transactional(readOnly = true)
public IPage<User> page(IPage<User> page, QueryWrapper<User> queryWrapper) {
return super.page(page, queryWrapper);
}
/**
* 统计查询 - 走读库
*/
@Override
@Transactional(readOnly = true)
public int count(QueryWrapper<User> queryWrapper) {
return super.count(queryWrapper);
}
/**
* 写操作 - 走写库
*/
@Override
@Transactional
public boolean save(User entity) {
return super.save(entity);
}
}
```
### 4.2 方案二:Controller层添加只读事务
```java
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private IUserService userService;
/**
* 查询接口 - 走读库
*/
@GetMapping("/list")
@Transactional(readOnly = true)
public Result<List<User>> getUserList() {
List<User> users = userService.list();
return Result.success(users);
}
/**
* 创建接口 - 走写库
*/
@PostMapping
@Transactional
public Result<Boolean> createUser(@RequestBody User user) {
boolean success = userService.save(user);
return Result.success(success);
}
}
```
### 4.3 方案三:创建专门的只读Service
```java
@Service
@Transactional(readOnly = true) // 类级别只读事务
public class UserReadOnlyService {
@Autowired
private UserMapper userMapper;
public List<User> queryUsers(QueryWrapper<User> queryWrapper) {
return userMapper.selectList(queryWrapper);
}
public IPage<User> queryUsersPage(IPage<User> page, QueryWrapper<User> queryWrapper) {
return userMapper.selectPage(page, queryWrapper);
}
public long queryCount(QueryWrapper<User> queryWrapper) {
return userMapper.selectCount(queryWrapper);
}
}
```
### 4.4 方案四:混合使用JDBC和MyBatis Plus
```java
@Service
public class UserHybridService {
@Autowired
private DataSource dataSource;
@Autowired
private IUserService userService;
/**
* 简单查询用JDBC - 走读库
*/
public long getUserCount() throws SQLException {
try (Connection conn = dataSource.getConnection()) {
conn.setReadOnly(true);
try (PreparedStatement stmt = conn.prepareStatement("SELECT COUNT(*) FROM user")) {
try (ResultSet rs = stmt.executeQuery()) {
return rs.next() ? rs.getLong(1) : 0;
}
}
}
}
/**
* 复杂操作用MyBatis Plus - 走写库
*/
@Transactional
public boolean createUserWithRelations(User user) {
return userService.save(user);
}
}
```
## 五、验证方法
### 5.1 测试代码
```java
@RestController
@RequestMapping("/api/test")
public class ReadWriteTestController {
@Autowired
private IUserService userService;
@Autowired
private DataSource dataSource;
/**
* 测试JDBC读操作
*/
@GetMapping("/jdbc-read")
public String testJdbcRead() throws SQLException {
try (Connection conn = dataSource.getConnection()) {
conn.setReadOnly(true);
// 执行查询...
return "JDBC读测试完成";
}
}
/**
* 测试MyBatis Plus读操作
*/
@GetMapping("/mybatis-read")
@Transactional(readOnly = true)
public String testMybatisRead() {
userService.list();
return "MyBatis Plus读测试完成";
}
}
```
### 5.2 期望的日志输出
**走读库的日志**:
```
TRACE ReadWriteSplittingPlugin - Reader connection set to 'cluster-ro-endpoint:3306'
TRACE ReadWriteSplittingPlugin - Routing read operation to reader endpoint
```
**走写库的日志**:
```
TRACE ReadWriteSplittingPlugin - Writer connection set to 'cluster-endpoint:3306'
TRACE ReadWriteSplittingPlugin - Routing write operation to writer endpoint
```
## 六、最佳实践
### 6.1 设计原则
1. **查询操作**:统一添加`@Transactional(readOnly = true)`
2. **写操作**:使用`@Transactional`或不添加注解
3. **事务边界**:在Service层或Controller层明确定义
4. **职责分离**:考虑创建专门的只读Service类
### 6.2 实施优先级
**高优先级**:
- 核心业务Service(订单、支付、用户等)
- 高频查询接口
- 报表和统计功能
**中优先级**:
- 基础数据Service
- 管理后台查询
- 定时任务查询
**低优先级**:
- 低频管理功能
- 工具类查询
### 6.3 注意事项
1. **事务传播**:在事务中的所有操作都会走主库
2. **连接复用**:HikariCP可能复用连接,观察日志时注意时间戳
3. **故障转移**:读库不可用时会自动转移到主库
4. **复制延迟**:业务逻辑需要考虑主从复制延迟
## 七、Spring JPA对比
### 7.1 为什么Spring JPA更容易实现读写分离
```java
// Spring Data JPA - 自动只读
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 框架自动为查询方法添加 @Transactional(readOnly = true)
List<User> findByStatus(String status); // 自动走读库
// 框架自动为写方法添加 @Transactional
User save(User user); // 自动走写库
}
```
### 7.2 JPA vs MyBatis Plus总结
| 特性 | Spring JPA | MyBatis Plus |
|------|------------|--------------|
| 学习曲线 | 简单,开箱即用 | 需要理解事务配置 |
| 自动化程度 | 高度自动化 | 需要手动配置 |
| 性能控制 | 抽象层较厚 | 更接近原生SQL |
| 读写分离 | 自动支持 | 需要手动实现 |
| SQL优化 | 相对困难 | 灵活度高 |
## 八、故障排查
### 8.1 常见问题
**问题1:看不到ReadWriteSplittingPlugin日志**
- 检查URL中的`wrapperLogLevel=FINEST`
- 确认logback.xml中的日志级别
- 重启应用重新观察
**问题2:所有操作都连接同一endpoint**
- 检查`@Transactional(readOnly = true)`是否正确添加
- 确认Aurora集群是否有只读副本
- 验证URL中的`wrapperPlugins`参数
**问题3:连接失败**
- 检查SSL证书配置
- 验证网络连通性
- 确认Aurora集群状态
### 8.2 调试技巧
1. **启用详细日志**:
```yaml
logging:
level:
software.amazon.jdbc: TRACE
com.zaxxer.hikari: DEBUG
```
2. **诊断连接状态**:
```java
@GetMapping("/diagnose")
public Map<String, Object> diagnoseDatasource() {
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
Map<String, Object> info = new HashMap<>();
info.put("driverName", metaData.getDriverName());
info.put("url", metaData.getURL());
info.put("isAwsWrapper", metaData.getDriverName().contains("Amazon"));
return info;
}
}
```
## 九、总结
AWS JDBC Wrapper是一个强大的数据库连接工具,但在MyBatis Plus环境下需要正确配置事务注解才能实现读写分离。核心要点:
1. **理解原理**:读写分离依赖`Connection.setReadOnly()`状态
2. **正确配置**:为查询方法添加`@Transactional(readOnly = true)`
3. **验证效果**:通过日志确认路由行为
4. **渐进实施**:按优先级逐步改造现有代码
通过本文的配置方案,可以有效提升系统的数据库读性能,减轻主库压力,为系统的高可用和高性能打下坚实基础。
---
> **作者经验**:在实际项目中,建议先在测试环境验证配置,观察日志确认读写分离生效后,再逐步推广到生产环境。同时要注意监控Aurora集群的读写负载分布,确保达到预期的性能提升效果。
**技术栈**:Spring Boot 2.x + MyBatis Plus 3.4.x + AWS JDBC Wrapper 2.2.x + Aurora MySQL