# 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