外观
PostgreSQL 应用层读写分离
核心概念
应用层读写分离是指在应用程序代码中实现数据库的读写分离逻辑,将读请求路由到从库,写请求路由到主库。这种方式具有以下特点:
- 灵活可控:应用程序可以根据业务逻辑灵活控制读写路由策略
- 无额外中间件:不需要引入专门的读写分离中间件,降低架构复杂度
- 开发成本高:需要在应用代码中实现读写分离逻辑
- 维护成本高:需要在应用层处理主从延迟、故障切换等问题
实现方案
1. Spring Boot + MyBatis 实现
1.1 数据源配置
yaml
# application.yml
spring:
datasource:
master:
url: jdbc:postgresql://master-db:5432/testdb
username: postgres
password: password
driver-class-name: org.postgresql.Driver
slave:
url: jdbc:postgresql://slave-db:5432/testdb
username: postgres
password: password
driver-class-name: org.postgresql.Driver1.2 动态数据源配置
java
// DynamicDataSource.java
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
// DataSourceContextHolder.java
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get() != null ? CONTEXT_HOLDER.get() : "master";
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}1.3 数据源切换注解
java
// DataSource.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master";
}
// DataSourceAspect.java
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(dataSource)")
public void beforeSwitchDataSource(DataSource dataSource) {
DataSourceContextHolder.setDataSourceType(dataSource.value());
}
@After("@annotation(dataSource)")
public void afterSwitchDataSource(DataSource dataSource) {
DataSourceContextHolder.clearDataSourceType();
}
}1.4 使用示例
java
// UserMapper.java
public interface UserMapper {
@DataSource("master")
int insert(User user);
@DataSource("slave")
User selectById(Long id);
@DataSource("slave")
List<User> selectAll();
@DataSource("master")
int update(User user);
@DataSource("master")
int deleteById(Long id);
}2. Hibernate 实现读写分离
2.1 配置文件
xml
<!-- hibernate.cfg.xml -->
<hibernate-configuration>
<session-factory>
<!-- 主库配置 -->
<property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
<property name="hibernate.connection.url">jdbc:postgresql://master-db:5432/testdb</property>
<property name="hibernate.connection.username">postgres</property>
<property name="hibernate.connection.password">password</property>
<!-- 从库配置 -->
<property name="hibernate.connection.datasource_slave">jdbc:postgresql://slave-db:5432/testdb</property>
<property name="hibernate.connection.username_slave">postgres</property>
<property name="hibernate.connection.password_slave">password</property>
<!-- 其他Hibernate配置 -->
<property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property>
<property name="hibernate.show_sql">true</property>
<property name="hibernate.format_sql">true</property>
</session-factory>
</hibernate-configuration>2.2 自定义SessionFactory
java
// ReadWriteSessionFactory.java
public class ReadWriteSessionFactory {
private SessionFactory masterSessionFactory;
private SessionFactory slaveSessionFactory;
public Session getSession(boolean readOnly) {
return readOnly ? slaveSessionFactory.openSession() : masterSessionFactory.openSession();
}
// 初始化方法
public void init() {
// 初始化主库SessionFactory
Configuration masterConfig = new Configuration().configure();
masterSessionFactory = masterConfig.buildSessionFactory();
// 初始化从库SessionFactory
Configuration slaveConfig = new Configuration().configure();
slaveConfig.setProperty("hibernate.connection.url", slaveUrl);
slaveConfig.setProperty("hibernate.connection.username", slaveUsername);
slaveConfig.setProperty("hibernate.connection.password", slavePassword);
slaveSessionFactory = slaveConfig.buildSessionFactory();
}
}3. 基于AOP的通用实现
3.1 依赖配置
xml
<!-- pom.xml -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>3.2 读写分离切面
java
// ReadWriteSplitAspect.java
@Aspect
@Component
public class ReadWriteSplitAspect {
@Pointcut("execution(* com.example.mapper.*.select*(..)) || execution(* com.example.mapper.*.find*(..)) || execution(* com.example.mapper.*.query*(..))")
public void readPointcut() {}
@Pointcut("execution(* com.example.mapper.*.insert*(..)) || execution(* com.example.mapper.*.update*(..)) || execution(* com.example.mapper.*.delete*(..))")
public void writePointcut() {}
@Before("readPointcut()")
public void setReadDataSource() {
DataSourceContextHolder.setDataSourceType("slave");
}
@Before("writePointcut()")
public void setWriteDataSource() {
DataSourceContextHolder.setDataSourceType("master");
}
@After("readPointcut() || writePointcut()")
public void clearDataSource() {
DataSourceContextHolder.clearDataSourceType();
}
}主从延迟处理
1. 延迟检测
java
// 检测主从延迟
public boolean isSlaveDelayed() {
// 从主库获取当前LSN
String masterLsn = jdbcTemplateMaster.queryForObject("SELECT pg_current_wal_lsn();", String.class);
// 从从库获取已复制的LSN
String slaveLsn = jdbcTemplateSlave.queryForObject("SELECT pg_last_wal_receive_lsn();", String.class);
// 计算延迟字节数
long masterLsnValue = Long.parseLong(masterLsn.replaceAll("/", ""), 16);
long slaveLsnValue = Long.parseLong(slaveLsn.replaceAll("/", ""), 16);
// 如果延迟超过1MB,则认为从库延迟
return (masterLsnValue - slaveLsnValue) > 1024 * 1024;
}2. 延迟处理策略
- 强制走主库:当检测到从库延迟超过阈值时,读请求也路由到主库
- 等待延迟恢复:等待从库延迟恢复后再执行读请求
- 数据一致性级别设置:允许用户在业务层面设置数据一致性级别
最佳实践
1. 生产环境配置建议
- 合理设计路由策略:根据业务特点设计读写分离路由策略,避免过度复杂
- 实现从库负载均衡:当有多个从库时,实现从库的负载均衡
- 处理主从延迟:实现主从延迟检测和处理机制,确保数据一致性
- 实现故障切换:当主库或从库故障时,能够自动切换到可用的数据库
- 监控和告警:监控主从延迟、数据库连接数、查询性能等指标
2. 性能优化
- 减少主从同步数据量:合理设计数据库结构,减少不必要的数据同步
- 优化WAL日志:调整wal_level、synchronous_commit等参数,平衡数据安全性和性能
- 使用连接池:为每个数据库实例配置独立的连接池
- 实现查询缓存:对热点数据实现应用层缓存,减少数据库查询压力
3. 安全性考虑
- 权限分离:为读写分离配置不同的数据库用户,实现权限分离
- 加密传输:使用SSL/TLS加密数据库连接,确保数据传输安全
- 定期更换密码:定期更换数据库密码,提高安全性
常见问题(FAQ)
Q1:如何处理主从延迟导致的数据不一致问题?
A1:可以采用以下几种方式处理:
- 强制走主库:对于实时性要求高的业务,可以强制读请求走主库
- 延迟检测:实现主从延迟检测,当延迟超过阈值时,自动切换到主库
- 数据版本控制:在业务层面实现数据版本控制,确保读取到最新版本的数据
- 使用同步复制:在主库配置synchronous_commit=on,确保主从数据实时同步,但会影响性能
Q2:如何实现多个从库的负载均衡?
A2:可以在动态数据源中实现从库的负载均衡,例如:
java
// 随机选择从库
private List<String> slaveDataSources = Arrays.asList("slave1", "slave2", "slave3");
private Random random = new Random();
public String getSlaveDataSource() {
return slaveDataSources.get(random.nextInt(slaveDataSources.size()));
}Q3:应用层读写分离和中间件读写分离有什么区别?
A3:主要区别如下:
| 特性 | 应用层读写分离 | 中间件读写分离 |
|---|---|---|
| 架构复杂度 | 低 | 高 |
| 开发成本 | 高 | 低 |
| 维护成本 | 高 | 中 |
| 灵活性 | 高 | 中 |
| 性能 | 高 | 中 |
| 适用场景 | 小型应用、业务逻辑简单 | 大型应用、业务逻辑复杂 |
Q4:如何处理事务中的读写请求?
A4:在同一个事务中,应该确保所有数据库操作都使用同一个数据源,避免出现数据不一致问题。可以在事务开始时设置数据源类型,并在事务结束时清除。
Q5:如何测试应用层读写分离的正确性?
A5:可以采用以下测试方法:
- 日志记录:在数据源切换时记录日志,验证读写请求是否路由到了正确的数据库
- 数据验证:在从库中插入测试数据,验证读请求是否能读取到从库数据
- 性能测试:对比读写分离前后的性能,验证是否达到预期效果
- 故障测试:模拟主库或从库故障,验证系统是否能正常工作
Q6:Spring Boot 3.x 中如何实现读写分离?
A6:Spring Boot 3.x 中可以使用以下方式实现:
- 使用 Spring Data JDBC 结合自定义数据源路由
- 使用 MyBatis-Plus 的动态数据源功能
- 使用 HikariCP 结合自定义路由策略
- 使用新一代的 Spring R2DBC 实现响应式读写分离
