外观
应用层读写分离
概述
应用层读写分离是指在应用程序层面实现数据库的读写分离,将写操作路由到主数据库,将读操作路由到一个或多个备数据库。应用层读写分离可以根据业务需求灵活调整读写策略,支持复杂的业务场景,同时避免了中间件或数据库层面的限制。
架构设计
读写分离架构
应用层读写分离架构主要包括以下组件:
- 主数据库:处理所有写操作和部分读操作
- 备数据库:处理只读操作,可以是Oracle或其他异构数据库
- 应用层:实现读写分离逻辑,将写操作发送到主库,将读操作发送到备库
- 数据复制机制:确保主库和备库之间的数据一致性,如Data Guard、GoldenGate等
- 连接池:管理数据库连接,提高连接利用率
- 负载均衡策略:将读操作均匀分布到多个备库
读写分离模式
应用层读写分离支持以下模式:
- 静态读写分离:基于配置文件静态配置读写分离规则,不随运行时变化
- 动态读写分离:根据运行时条件动态调整读写分离规则,如备库延迟、负载等
- 基于事务的读写分离:在事务内的所有操作都路由到主库,确保事务一致性
- 基于SQL类型的读写分离:根据SQL语句类型(SELECT、INSERT、UPDATE、DELETE)路由到不同的数据库
- 基于业务逻辑的读写分离:根据业务逻辑路由到不同的数据库,如将报表查询路由到备库
实现方法
Java应用实现
使用Spring框架实现读写分离
Spring框架提供了AbstractRoutingDataSource类,可以实现动态数据源路由。以下是基于Spring Boot的实现示例:
- 定义数据源类型枚举
java
public enum DataSourceType {
PRIMARY, // 主库
STANDBY // 备库
}- 定义数据源上下文持有者
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();
}
}- 实现动态数据源路由
java
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}- 配置数据源
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);
}
}- 配置事务切面
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();
}
}- 配置文件
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的实现示例:
- 定义数据源插件
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) {
// 设置属性
}
}- 注册插件
java
@Configuration
public class MyBatisConfig {
@Bean
public DataSourcePlugin dataSourcePlugin() {
return new DataSourcePlugin();
}
}.NET应用实现
使用Entity Framework Core实现读写分离
Entity Framework Core可以通过自定义DbContext实现读写分离。以下是基于.NET Core的实现示例:
- 定义读写分离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; }
}- 配置依赖注入
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);
};
});
}- 使用读写分离服务
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 19c | Oracle 21c |
|---|---|---|
| 连接池性能 | 一般 | 显著提升,优化了连接池管理 |
| 并行查询 | 支持 | 增强并行查询,提高备库查询性能 |
| 自动索引 | 支持主库自动索引,备库不支持 | 支持备库自动索引,提高备库查询性能 |
| 实时统计信息 | 主库实时统计信息,备库延迟更新 | 增强备库实时统计信息,减少统计信息延迟 |
| 内存管理 | 基础内存管理 | 增强内存管理,优化备库内存使用 |
| 云集成 | 基础云集成 | 增强云集成,支持与OCI、AWS、Azure等云平台的应用层读写分离 |
| 监控能力 | 基础监控能力 | 增强监控能力,提供更详细的性能指标 |
监控与管理
监控读写分离状态
- 监控备库延迟
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- 监控备库负载
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;- 监控应用层读写分离状态
- 使用应用性能监控工具(如APM)监控读写分离的流量分布
- 在应用中添加日志,记录每个请求的数据源路由情况
- 定期统计读操作和写操作的分布比例
常见管理任务
调整读写分离策略
- 根据业务需求调整读操作和写操作的分布比例
- 调整备库的负载均衡策略
- 实现动态读写分离,根据备库延迟自动调整路由规则
处理备库故障
- 实现备库故障自动切换,当备库不可用时自动切换到其他备库或主库
- 监控备库状态,及时发现和解决备库故障
- 定期测试备库切换流程,确保切换正常
优化应用层读写分离
- 优化连接池配置,提高连接利用率
- 实现连接复用,减少连接创建和销毁的开销
- 优化SQL查询,减少数据库负载
最佳实践
性能优化
优化数据库连接
- 使用连接池管理数据库连接,避免频繁创建和销毁连接
- 配置合适的连接池大小,根据系统负载调整
- 实现连接复用,减少连接开销
优化SQL查询
- 优化读操作SQL,减少数据库负载
- 使用索引优化查询性能
- 避免在备库上执行复杂的写操作
优化备库性能
- 配置足够的内存和CPU资源
- 使用高速存储设备,提高I/O性能
- 优化备库的Oracle参数
- 启用并行查询,提高复杂查询的性能
高可用性
实现故障自动切换
- 监控备库状态,当备库不可用时自动切换到其他备库或主库
- 实现连接重试机制,提高系统的容错能力
- 配置多个备库,实现读写分离的高可用性
确保数据一致性
- 选择合适的数据复制机制,确保主库和备库之间的数据一致性
- 实现基于事务的读写分离,确保事务内的操作都路由到主库
- 定期验证主库和备库的数据一致性
定期测试切换流程
- 定期执行备库切换测试,确保切换流程正常
- 测试应用层读写分离在切换过程中的表现,确保业务连续性
安全管理
限制备库用户权限
- 备库用户只授予只读权限,防止误操作
- 定期轮换数据库用户密码
- 使用最小权限原则,只授予必要的权限
加密传输
- 配置客户端和数据库之间的连接加密
- 使用SSL/TLS协议保护网络传输
- 加密敏感数据,防止数据泄露
监控访问日志
- 监控应用层的数据库访问日志
- 配置审计日志,记录所有数据库操作
- 定期分析访问日志,发现潜在的安全风险
故障处理
常见故障及解决方法
- 备库延迟过大
症状:备库查询结果与主库不一致,影响业务数据准确性。
解决方法:
- 检查数据复制机制的状态,确保数据复制正常
- 调整数据复制的并行度,提高复制速度
- 优化网络连接,减少传输延迟
- 临时将读操作路由到主库,直到备库延迟恢复正常
- 备库不可用
症状:应用无法连接到备库,读写分离失效,所有请求都路由到主库。
解决方法:
- 检查备库的状态,重启备库服务
- 检查网络连接,确保备库可以正常访问
- 切换到其他备库(如果配置了多个备库)
- 暂时将所有请求路由到主库,直到备库恢复正常
- 应用层路由错误
症状:写操作被路由到备库,或者读操作被路由到主库,影响系统正常运行。
解决方法:
- 检查应用层的读写分离配置,确保路由规则正确
- 检查应用层的事务管理,确保事务内的操作都路由到主库
- 查看应用日志,定位路由错误的原因
- 修复路由逻辑,重新部署应用
常见问题(FAQ)
Q: 应用层读写分离有什么优势?
A: 应用层读写分离具有以下优势:
- 灵活性高,可以根据业务需求灵活调整读写策略
- 支持复杂的业务场景,如基于业务逻辑的读写分离
- 避免了中间件或数据库层面的限制
- 可以实现更细粒度的读写分离,如基于用户、基于业务模块等
- 便于监控和调试,问题定位更容易
Q: 应用层读写分离适用于哪些场景?
A: 应用层读写分离适用于以下场景:
- 读多写少的业务场景,如电商网站、新闻网站等
- 对读写分离策略有特殊要求的场景
- 已有成熟的应用框架,可以方便地实现读写分离
- 希望对读写分离有完全控制的场景
Q: 如何确保应用层读写分离的数据一致性?
A: 可以通过以下方式确保数据一致性:
- 选择合适的数据复制机制,确保主库和备库之间的数据一致性
- 实现基于事务的读写分离,确保事务内的所有操作都路由到主库
- 实现读写分离的延迟监控,当备库延迟超过阈值时自动调整路由规则
- 定期验证主库和备库的数据一致性
Q: 应用层读写分离的性能如何?
A: 应用层读写分离的性能取决于多种因素,包括应用层的实现方式、数据库的性能、网络连接等。在优化配置的情况下,应用层读写分离可以显著提高系统的整体吞吐量,减少主数据库的负载。
Q: 如何实现应用层读写分离的动态调整?
A: 可以通过以下方式实现动态调整:
- 监控备库的延迟和负载,根据实时情况调整路由规则
- 实现基于规则的动态路由,如当备库延迟超过阈值时将读操作路由到主库
- 使用配置中心实现动态配置,无需重启应用即可调整读写分离规则
- 实现自适应读写分离,根据系统负载自动调整读写比例
总结
应用层读写分离是一种灵活、高效的数据库负载均衡方案,通过在应用程序层面实现读写分离,可以根据业务需求灵活调整读写策略,支持复杂的业务场景。在实现应用层读写分离时,需要考虑数据一致性、性能优化、高可用性和安全性等因素,选择合适的实现方式和配置参数。Oracle 21c相比19c在应用层读写分离方面有显著增强,包括提高备库查询性能、支持备库自动索引和增强实时统计信息等功能。
