Skip to content

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.Driver

1.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:可以采用以下几种方式处理:

  1. 强制走主库:对于实时性要求高的业务,可以强制读请求走主库
  2. 延迟检测:实现主从延迟检测,当延迟超过阈值时,自动切换到主库
  3. 数据版本控制:在业务层面实现数据版本控制,确保读取到最新版本的数据
  4. 使用同步复制:在主库配置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:可以采用以下测试方法:

  1. 日志记录:在数据源切换时记录日志,验证读写请求是否路由到了正确的数据库
  2. 数据验证:在从库中插入测试数据,验证读请求是否能读取到从库数据
  3. 性能测试:对比读写分离前后的性能,验证是否达到预期效果
  4. 故障测试:模拟主库或从库故障,验证系统是否能正常工作

Q6:Spring Boot 3.x 中如何实现读写分离?

A6:Spring Boot 3.x 中可以使用以下方式实现:

  1. 使用 Spring Data JDBC 结合自定义数据源路由
  2. 使用 MyBatis-Plus 的动态数据源功能
  3. 使用 HikariCP 结合自定义路由策略
  4. 使用新一代的 Spring R2DBC 实现响应式读写分离