Skip to content

应用层读写分离

概述

应用层读写分离是指在应用程序层面实现数据库的读写分离,将写操作路由到主数据库,将读操作路由到一个或多个备数据库。应用层读写分离可以根据业务需求灵活调整读写策略,支持复杂的业务场景,同时避免了中间件或数据库层面的限制。

架构设计

读写分离架构

应用层读写分离架构主要包括以下组件:

  • 主数据库:处理所有写操作和部分读操作
  • 备数据库:处理只读操作,可以是Oracle或其他异构数据库
  • 应用层:实现读写分离逻辑,将写操作发送到主库,将读操作发送到备库
  • 数据复制机制:确保主库和备库之间的数据一致性,如Data Guard、GoldenGate等
  • 连接池:管理数据库连接,提高连接利用率
  • 负载均衡策略:将读操作均匀分布到多个备库

读写分离模式

应用层读写分离支持以下模式:

  • 静态读写分离:基于配置文件静态配置读写分离规则,不随运行时变化
  • 动态读写分离:根据运行时条件动态调整读写分离规则,如备库延迟、负载等
  • 基于事务的读写分离:在事务内的所有操作都路由到主库,确保事务一致性
  • 基于SQL类型的读写分离:根据SQL语句类型(SELECT、INSERT、UPDATE、DELETE)路由到不同的数据库
  • 基于业务逻辑的读写分离:根据业务逻辑路由到不同的数据库,如将报表查询路由到备库

实现方法

Java应用实现

使用Spring框架实现读写分离

Spring框架提供了AbstractRoutingDataSource类,可以实现动态数据源路由。以下是基于Spring Boot的实现示例:

  1. 定义数据源类型枚举
java
public enum DataSourceType {
    PRIMARY,  // 主库
    STANDBY   // 备库
}
  1. 定义数据源上下文持有者
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();
    }

    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}
  1. 实现动态数据源路由
java
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}
  1. 配置数据源
java
@Configuration
public class DataSourceConfig {
    // 配置主库数据源
    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 配置备库数据源
    @Bean(name = "standbyDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.standby")
    public DataSource standbyDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 配置动态数据源
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource,
                                       @Qualifier("standbyDataSource") DataSource standbyDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.PRIMARY, primaryDataSource);
        targetDataSources.put(DataSourceType.STANDBY, standbyDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource);
        return dynamicDataSource;
    }

    // 配置JdbcTemplate
    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        return new JdbcTemplate(dynamicDataSource);
    }
}
  1. 配置事务切面
java
@Aspect
@Component
public class DataSourceAspect {
    // 读操作路由到备库
    @Before("execution(* com.example.service.*.select*(..)) || execution(* com.example.service.*.get*(..)) || execution(* com.example.service.*.query*(..))")
    public void setReadDataSourceType() {
        DataSourceContextHolder.setDataSourceType(DataSourceType.STANDBY);
    }

    // 写操作路由到主库
    @Before("execution(* com.example.service.*.insert*(..)) || execution(* com.example.service.*.update*(..)) || execution(* com.example.service.*.delete*(..))")
    public void setWriteDataSourceType() {
        DataSourceContextHolder.setDataSourceType(DataSourceType.PRIMARY);
    }

    // 清除数据源类型
    @After("execution(* com.example.service.*.*(..))")
    public void clearDataSourceType() {
        DataSourceContextHolder.clearDataSourceType();
    }
}
  1. 配置文件
yaml
spring:
  datasource:
    # 主库数据源配置
    primary:
      url: jdbc:oracle:thin:@primary_host:1521:primary_db
      username: scott
      password: tiger
      driver-class-name: oracle.jdbc.OracleDriver
    # 备库数据源配置
    standby:
      url: jdbc:oracle:thin:@standby_host:1521:standby_db
      username: scott
      password: tiger
      driver-class-name: oracle.jdbc.OracleDriver

使用MyBatis实现读写分离

MyBatis可以通过插件机制实现读写分离。以下是基于MyBatis的实现示例:

  1. 定义数据源插件
java
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DataSourcePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        
        // 根据SQL类型设置数据源
        if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
            DataSourceContextHolder.setDataSourceType(DataSourceType.STANDBY);
        } else {
            DataSourceContextHolder.setDataSourceType(DataSourceType.PRIMARY);
        }
        
        try {
            return invocation.proceed();
        } finally {
            DataSourceContextHolder.clearDataSourceType();
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 设置属性
    }
}
  1. 注册插件
java
@Configuration
public class MyBatisConfig {
    @Bean
    public DataSourcePlugin dataSourcePlugin() {
        return new DataSourcePlugin();
    }
}

.NET应用实现

使用Entity Framework Core实现读写分离

Entity Framework Core可以通过自定义DbContext实现读写分离。以下是基于.NET Core的实现示例:

  1. 定义读写分离DbContext
csharp
public class AppDbContext : DbContext
{
    private readonly bool _isReadOnly;

    // 主库构造函数
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
        _isReadOnly = false;
    }

    // 备库构造函数
    public AppDbContext(DbContextOptions<AppDbContext> options, bool isReadOnly) : base(options)
    {
        _isReadOnly = isReadOnly;
    }

    // 重写SaveChanges方法,只读模式下不允许保存
    public override int SaveChanges()
    {
        if (_isReadOnly)
        {
            throw new InvalidOperationException("Cannot save changes in read-only mode.");
        }
        return base.SaveChanges();
    }

    // 定义实体
    public DbSet<Employee> Employees { get; set; }
    public DbSet<Department> Departments { get; set; }
}
  1. 配置依赖注入
csharp
public void ConfigureServices(IServiceCollection services)
{
    // 配置主库连接字符串
    string primaryConnectionString = Configuration.GetConnectionString("PrimaryDatabase");
    // 配置备库连接字符串
    string standbyConnectionString = Configuration.GetConnectionString("StandbyDatabase");

    // 注册主库DbContext
    services.AddDbContext<AppDbContext>(options =>
        options.UseOracle(primaryConnectionString));

    // 注册备库DbContext(只读模式)
    services.AddDbContext<AppDbContext>(options =>
        options.UseOracle(standbyConnectionString),
        contextLifetime: ServiceLifetime.Transient,
        optionsLifetime: ServiceLifetime.Singleton);

    // 注册读写分离服务
    services.AddScoped<Func<bool, AppDbContext>>((serviceProvider) =>
    {
        return (isReadOnly) =>
        {
            var options = serviceProvider.GetRequiredService<DbContextOptions<AppDbContext>>();
            return new AppDbContext(options, isReadOnly);
        };
    });
}
  1. 使用读写分离服务
csharp
public class EmployeeService
{
    private readonly Func<bool, AppDbContext> _dbContextFactory;

    public EmployeeService(Func<bool, AppDbContext> dbContextFactory)
    {
        _dbContextFactory = dbContextFactory;
    }

    // 读操作使用备库
    public async Task<List<Employee>> GetEmployeesAsync()
    {
        using (var dbContext = _dbContextFactory(true))
        {
            return await dbContext.Employees.ToListAsync();
        }
    }

    // 写操作使用主库
    public async Task AddEmployeeAsync(Employee employee)
    {
        using (var dbContext = _dbContextFactory(false))
        {
            dbContext.Employees.Add(employee);
            await dbContext.SaveChangesAsync();
        }
    }
}

版本差异

Oracle 19c vs 21c应用层读写分离差异

特性Oracle 19cOracle 21c
连接池性能一般显著提升,优化了连接池管理
并行查询支持增强并行查询,提高备库查询性能
自动索引支持主库自动索引,备库不支持支持备库自动索引,提高备库查询性能
实时统计信息主库实时统计信息,备库延迟更新增强备库实时统计信息,减少统计信息延迟
内存管理基础内存管理增强内存管理,优化备库内存使用
云集成基础云集成增强云集成,支持与OCI、AWS、Azure等云平台的应用层读写分离
监控能力基础监控能力增强监控能力,提供更详细的性能指标

监控与管理

监控读写分离状态

  1. 监控备库延迟
sql
-- Data Guard备库延迟监控
SELECT NAME, VALUE FROM V$DATAGUARD_STATS WHERE NAME IN ('transport lag', 'apply lag');

-- GoldenGate备库延迟监控
ggsci
LAG EXTRACT ext1
LAG REPLICAT rep1
  1. 监控备库负载
sql
-- 查看备库的CPU使用率
SELECT VALUE FROM V$SYSSTAT WHERE NAME = 'CPU used by this session';

-- 查看备库的内存使用率
SELECT * FROM V$SGASTAT WHERE NAME LIKE '%free%';

-- 查看备库的I/O等待事件
SELECT EVENT, COUNT(*) FROM V$SESSION_WAIT GROUP BY EVENT ORDER BY COUNT(*) DESC;
  1. 监控应用层读写分离状态
  • 使用应用性能监控工具(如APM)监控读写分离的流量分布
  • 在应用中添加日志,记录每个请求的数据源路由情况
  • 定期统计读操作和写操作的分布比例

常见管理任务

  1. 调整读写分离策略

    • 根据业务需求调整读操作和写操作的分布比例
    • 调整备库的负载均衡策略
    • 实现动态读写分离,根据备库延迟自动调整路由规则
  2. 处理备库故障

    • 实现备库故障自动切换,当备库不可用时自动切换到其他备库或主库
    • 监控备库状态,及时发现和解决备库故障
    • 定期测试备库切换流程,确保切换正常
  3. 优化应用层读写分离

    • 优化连接池配置,提高连接利用率
    • 实现连接复用,减少连接创建和销毁的开销
    • 优化SQL查询,减少数据库负载

最佳实践

性能优化

  1. 优化数据库连接

    • 使用连接池管理数据库连接,避免频繁创建和销毁连接
    • 配置合适的连接池大小,根据系统负载调整
    • 实现连接复用,减少连接开销
  2. 优化SQL查询

    • 优化读操作SQL,减少数据库负载
    • 使用索引优化查询性能
    • 避免在备库上执行复杂的写操作
  3. 优化备库性能

    • 配置足够的内存和CPU资源
    • 使用高速存储设备,提高I/O性能
    • 优化备库的Oracle参数
    • 启用并行查询,提高复杂查询的性能

高可用性

  1. 实现故障自动切换

    • 监控备库状态,当备库不可用时自动切换到其他备库或主库
    • 实现连接重试机制,提高系统的容错能力
    • 配置多个备库,实现读写分离的高可用性
  2. 确保数据一致性

    • 选择合适的数据复制机制,确保主库和备库之间的数据一致性
    • 实现基于事务的读写分离,确保事务内的操作都路由到主库
    • 定期验证主库和备库的数据一致性
  3. 定期测试切换流程

    • 定期执行备库切换测试,确保切换流程正常
    • 测试应用层读写分离在切换过程中的表现,确保业务连续性

安全管理

  1. 限制备库用户权限

    • 备库用户只授予只读权限,防止误操作
    • 定期轮换数据库用户密码
    • 使用最小权限原则,只授予必要的权限
  2. 加密传输

    • 配置客户端和数据库之间的连接加密
    • 使用SSL/TLS协议保护网络传输
    • 加密敏感数据,防止数据泄露
  3. 监控访问日志

    • 监控应用层的数据库访问日志
    • 配置审计日志,记录所有数据库操作
    • 定期分析访问日志,发现潜在的安全风险

故障处理

常见故障及解决方法

  1. 备库延迟过大

症状:备库查询结果与主库不一致,影响业务数据准确性。

解决方法

  • 检查数据复制机制的状态,确保数据复制正常
  • 调整数据复制的并行度,提高复制速度
  • 优化网络连接,减少传输延迟
  • 临时将读操作路由到主库,直到备库延迟恢复正常
  1. 备库不可用

症状:应用无法连接到备库,读写分离失效,所有请求都路由到主库。

解决方法

  • 检查备库的状态,重启备库服务
  • 检查网络连接,确保备库可以正常访问
  • 切换到其他备库(如果配置了多个备库)
  • 暂时将所有请求路由到主库,直到备库恢复正常
  1. 应用层路由错误

症状:写操作被路由到备库,或者读操作被路由到主库,影响系统正常运行。

解决方法

  • 检查应用层的读写分离配置,确保路由规则正确
  • 检查应用层的事务管理,确保事务内的操作都路由到主库
  • 查看应用日志,定位路由错误的原因
  • 修复路由逻辑,重新部署应用

常见问题(FAQ)

Q: 应用层读写分离有什么优势?

A: 应用层读写分离具有以下优势:

  • 灵活性高,可以根据业务需求灵活调整读写策略
  • 支持复杂的业务场景,如基于业务逻辑的读写分离
  • 避免了中间件或数据库层面的限制
  • 可以实现更细粒度的读写分离,如基于用户、基于业务模块等
  • 便于监控和调试,问题定位更容易

Q: 应用层读写分离适用于哪些场景?

A: 应用层读写分离适用于以下场景:

  • 读多写少的业务场景,如电商网站、新闻网站等
  • 对读写分离策略有特殊要求的场景
  • 已有成熟的应用框架,可以方便地实现读写分离
  • 希望对读写分离有完全控制的场景

Q: 如何确保应用层读写分离的数据一致性?

A: 可以通过以下方式确保数据一致性:

  • 选择合适的数据复制机制,确保主库和备库之间的数据一致性
  • 实现基于事务的读写分离,确保事务内的所有操作都路由到主库
  • 实现读写分离的延迟监控,当备库延迟超过阈值时自动调整路由规则
  • 定期验证主库和备库的数据一致性

Q: 应用层读写分离的性能如何?

A: 应用层读写分离的性能取决于多种因素,包括应用层的实现方式、数据库的性能、网络连接等。在优化配置的情况下,应用层读写分离可以显著提高系统的整体吞吐量,减少主数据库的负载。

Q: 如何实现应用层读写分离的动态调整?

A: 可以通过以下方式实现动态调整:

  • 监控备库的延迟和负载,根据实时情况调整路由规则
  • 实现基于规则的动态路由,如当备库延迟超过阈值时将读操作路由到主库
  • 使用配置中心实现动态配置,无需重启应用即可调整读写分离规则
  • 实现自适应读写分离,根据系统负载自动调整读写比例

总结

应用层读写分离是一种灵活、高效的数据库负载均衡方案,通过在应用程序层面实现读写分离,可以根据业务需求灵活调整读写策略,支持复杂的业务场景。在实现应用层读写分离时,需要考虑数据一致性、性能优化、高可用性和安全性等因素,选择合适的实现方式和配置参数。Oracle 21c相比19c在应用层读写分离方面有显著增强,包括提高备库查询性能、支持备库自动索引和增强实时统计信息等功能。