Skip to content

MySQL 应用层读写分离

读写分离的概念与原理

读写分离定义

读写分离是一种数据库架构设计模式,将数据库的读操作和写操作分离到不同的数据库实例上执行,以提高系统的整体性能和可用性。

读写分离原理

  1. 主库处理写操作:所有的INSERT、UPDATE、DELETE操作发送到主库
  2. 从库处理读操作:所有的SELECT操作发送到从库
  3. 数据同步:通过MySQL复制机制,主库的数据变更实时同步到从库
  4. 负载均衡:多个从库之间可以实现读操作的负载均衡

读写分离的优势

  • 提高系统性能:分散读写压力,充分利用服务器资源
  • 提高系统可用性:单个从库故障不影响写操作,主库故障可以快速切换
  • 提高扩展性:可以根据读负载灵活增加从库数量
  • 优化资源利用:写操作和读操作可以使用不同的硬件配置

应用层读写分离的实现方式

1. 代码层面实现

核心思路

在应用代码中直接实现读写分离逻辑,根据SQL语句类型将请求路由到不同的数据库连接。

实现方式

Java应用示例(Spring框架)

使用Spring的AbstractRoutingDataSource实现动态数据源路由:

java
// 1. 定义数据源类型枚举
public enum DataSourceType {
    MASTER,
    SLAVE
}

// 2. 定义线程本地变量存储当前数据源类型
public class DataSourceContextHolder {
    private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
    
    public static void setDataSourceType(DataSourceType type) {
        CONTEXT_HOLDER.set(type);
    }
    
    public static DataSourceType getDataSourceType() {
        return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER : CONTEXT_HOLDER.get();
    }
    
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

// 3. 实现动态数据源路由
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

// 4. 配置数据源
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource masterDataSource() {
        // 配置主库数据源
    }
    
    @Bean
    public DataSource slaveDataSource() {
        // 配置从库数据源
    }
    
    @Bean
    public DynamicDataSource dynamicDataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.MASTER, masterDataSource());
        dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource());
        
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        return dynamicDataSource;
    }
}

// 5. 使用AOP实现读写分离
@Aspect
@Component
public class DataSourceAspect {
    @Pointcut("execution(* com.example.service.*.select*(..)) || execution(* com.example.service.*.get*(..))")
    public void readPointcut() {}
    
    @Pointcut("execution(* com.example.service.*.insert*(..)) || execution(* com.example.service.*.update*(..)) || execution(* com.example.service.*.delete*(..))")
    public void writePointcut() {}
    
    @Before("readPointcut()")
    public void setReadDataSource() {
        DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
    }
    
    @Before("writePointcut()")
    public void setWriteDataSource() {
        DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
    }
    
    @After("readPointcut() || writePointcut()")
    public void clearDataSource() {
        DataSourceContextHolder.clearDataSourceType();
    }
}
Python应用示例(Django框架)

使用Django的数据库路由实现读写分离:

python
# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mydb',
        'USER': 'root',
        'PASSWORD': 'password',
        'HOST': 'master.example.com',
        'PORT': '3306',
    },
    'slave': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mydb',
        'USER': 'root',
        'PASSWORD': 'password',
        'HOST': 'slave.example.com',
        'PORT': '3306',
    }
}

# 自定义数据库路由
class ReadWriteRouter:
    def db_for_read(self, model, **hints):
        """读操作使用从库"""
        return 'slave'
    
    def db_for_write(self, model, **hints):
        """写操作使用主库"""
        return 'default'
    
    def allow_relation(self, obj1, obj2, **hints):
        """允许所有关系"""
        return True
    
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        """迁移操作使用主库"""
        return db == 'default'

# settings.py中配置路由
DATABASE_ROUTERS = ['myapp.routers.ReadWriteRouter']

2. ORM框架层面实现

核心思路

利用ORM框架提供的读写分离功能,无需修改业务代码即可实现读写分离。

主流ORM框架支持

MyBatis-Plus
yaml
# application.yml
spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://master:3306/mydb
          username: root
          password: password
        slave1:
          url: jdbc:mysql://slave1:3306/mydb
          username: root
          password: password
        slave2:
          url: jdbc:mysql://slave2:3306/mydb
          username: root
          password: password
      strategy:
        slave:
          - slave1
          - slave2

使用注解实现读写分离:

java
// 读操作使用从库
@DS("slave")
List<User> selectAll();

// 写操作使用主库
@DS("master")
int insert(User user);
Hibernate
xml
<!-- hibernate.cfg.xml -->
<session-factory>
    <!-- 主库数据源 -->
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://master:3306/mydb</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password">password</property>
    
    <!-- 从库数据源 -->
    <property name="hibernate.connection.slave.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.slave.url">jdbc:mysql://slave:3306/mydb</property>
    <property name="hibernate.connection.slave.username">root</property>
    <property name="hibernate.connection.slave.password">password</property>
    
    <!-- 配置读写分离拦截器 -->
    <property name="hibernate.connection.provider_class">com.example.ReadWriteConnectionProvider</property>
</session-factory>

3. 中间件层面实现

核心思路

使用专门的数据库中间件实现读写分离,应用程序通过中间件访问数据库,无需关心底层数据源路由。

主流中间件

ShardingSphere-JDBC
yaml
# application.yml
spring:
  shardingsphere:
    datasource:
      names: master,slave
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://master:3306/mydb
        username: root
        password: password
      slave:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave:3306/mydb
        username: root
        password: password
    rules:
      readwrite-splitting:
        data-sources:
          readwrite_ds:
            write-data-source-name: master
            read-data-source-names:
              - slave
            load-balancer-name: round_robin
        load-balancers:
          round_robin:
            type: ROUND_ROBIN
    props:
      sql-show: true

应用层读写分离的挑战与解决方案

1. 主从复制延迟问题

问题现象

  • 主库写入数据后,从库尚未同步,导致读操作获取到旧数据
  • 常见于大事务、高并发写入场景

解决方案

  • 写后读一致性:写操作后立即读取使用主库

    java
    // 写操作
    userService.update(user);
    // 强制使用主库读取
    DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
    User updatedUser = userService.getById(userId);
    DataSourceContextHolder.clearDataSourceType();
  • 延迟检测机制:定期检查从库延迟,超过阈值时自动切换到主库

    sql
    SHOW SLAVE STATUS\G
    -- 检查Seconds_Behind_Master字段
  • 使用半同步复制:确保至少一个从库收到二进制日志后才返回成功

    sql
    -- 启用半同步复制
    INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
    SET GLOBAL rpl_semi_sync_master_enabled = 1;

2. 事务一致性问题

问题现象

  • 同一个事务中既有读又有写,可能导致数据不一致
  • 事务隔离级别无法保证跨数据源的一致性

解决方案

  • 同一事务使用同一数据源:事务内的所有操作都使用主库

    java
    @Transactional
    public void updateAndQuery(User user) {
        // 写操作
        userMapper.updateById(user);
        // 读操作,同一事务内自动使用主库
        User updatedUser = userMapper.selectById(user.getId());
    }
  • 分布式事务:对于跨数据源的事务,使用分布式事务框架

    • Seata
    • Atomikos
    • Bitronix

3. 从库负载均衡问题

问题现象

  • 多个从库之间负载不均衡
  • 部分从库压力过大,影响性能

解决方案

  • 轮询算法:简单易用,适用于从库性能相近的场景
  • 随机算法:实现简单,负载分布均匀
  • 权重算法:根据从库性能设置不同权重
  • 最少连接数算法:根据当前连接数动态分配请求

4. 故障切换问题

问题现象

  • 主库或从库故障时,需要手动切换数据源
  • 切换不及时会导致系统不可用

解决方案

  • 健康检查机制:定期检查数据库实例状态

    java
    // 简单的健康检查
    public boolean isDataSourceHealthy(DataSource dataSource) {
        try (Connection conn = dataSource.getConnection()) {
            return conn.isValid(5);
        } catch (SQLException e) {
            return false;
        }
    }
  • 自动故障切换:检测到故障时自动切换到可用数据源

  • 监控与告警:及时通知DBA处理故障

不同MySQL版本的读写分离支持

MySQL 5.6

  • 支持异步复制和半同步复制
  • 半同步复制性能较差
  • 不支持并行复制(基于数据库的并行复制在5.6.3开始支持)

MySQL 5.7

  • 增强了半同步复制性能
  • 支持基于组提交的并行复制(slave_parallel_type = 'LOGICAL_CLOCK'
  • 支持多源复制
  • 复制延迟明显降低

MySQL 8.0

  • 支持基于写集合的并行复制,大幅提升复制效率
  • 增强了半同步复制功能
  • 支持复制通道加密
  • 提供了更丰富的复制监控指标
  • 复制延迟进一步降低

应用层读写分离的最佳实践

1. 架构设计

  • 评估业务需求:分析读写比例,确定是否需要读写分离
  • 选择合适的实现方式:根据技术栈和团队经验选择代码层面或中间件层面实现
  • 设计合理的从库数量:根据读负载和主库写性能确定从库数量
  • 考虑数据一致性要求:根据业务重要性选择合适的一致性级别

2. 配置优化

  • 优化主库配置

    txt
    [mysqld]
    # 启用二进制日志
    log_bin = mysql-bin
    # 选择合适的二进制日志格式
    binlog_format = ROW
    # 配置二进制日志过期时间
    expire_logs_days = 7
  • 优化从库配置

    txt
    [mysqld]
    # 启用从库只读
    read_only = 1
    # 配置并行复制
    slave_parallel_workers = 8
    slave_parallel_type = LOGICAL_CLOCK
    # 禁用从库二进制日志(如果不需要级联复制)
    skip_log_bin

3. 监控与维护

  • 监控复制延迟:设置合理的延迟告警阈值
  • 监控从库负载:避免单个从库压力过大
  • 定期进行主从切换演练:确保故障切换流程可靠
  • 定期清理二进制日志:避免磁盘空间不足

4. 性能优化

  • 使用连接池:减少数据库连接开销

    yaml
    spring:
      datasource:
        hikari:
          maximum-pool-size: 100
          minimum-idle: 10
          connection-timeout: 30000
          idle-timeout: 600000
          max-lifetime: 1800000
  • 优化查询语句:减少从库查询压力

  • 考虑使用缓存:减少数据库读操作

常见问题(FAQ)

Q1: 什么时候需要考虑读写分离?

A1: 当满足以下条件时可以考虑读写分离:

  • 读写比例失衡,读操作远多于写操作(一般读:写 > 10:1)
  • 单库性能无法满足业务需求
  • 业务对数据一致性要求不是特别严格
  • 有足够的服务器资源部署多个数据库实例

Q2: 应用层读写分离和中间件读写分离各有什么优缺点?

A2:

  • 应用层读写分离
    • 优点:实现灵活,性能开销小,无需额外组件
    • 缺点:侵入业务代码,维护成本高,迁移困难
  • 中间件读写分离
    • 优点:对业务透明,易于维护和扩展,功能丰富
    • 缺点:增加系统复杂度,性能开销较大,需要额外部署和维护中间件

Q3: 如何处理热点数据的读操作?

A3: 可以采取以下措施:

  • 使用缓存(Redis、Memcached)缓存热点数据
  • 增加热点数据所在表的从库数量
  • 考虑使用分库分表分散热点数据
  • 优化查询语句,减少热点数据的访问频率

Q4: 读写分离对应用性能有什么影响?

A4: 读写分离对应用性能的影响主要包括:

  • 正面影响:分散读写压力,提高系统整体性能和并发能力
  • 负面影响:增加了系统复杂度,可能引入主从延迟和一致性问题
  • 性能开销:数据源路由、健康检查等机制会带来一定的性能开销

Q5: 如何测试读写分离的效果?

A5: 可以从以下几个方面测试:

  • 功能测试:验证读操作路由到从库,写操作路由到主库
  • 性能测试:对比读写分离前后的系统性能
  • 压力测试:模拟高并发场景,测试系统稳定性
  • 故障测试:模拟主库或从库故障,测试故障切换效果
  • 一致性测试:验证数据一致性是否符合要求

Q6: 读写分离和分库分表有什么区别?

A6:

  • 读写分离:将读操作和写操作分离到不同实例,解决单库读写压力问题
  • 分库分表:将数据分散到多个数据库或表中,解决单库数据量过大问题
  • 关系:两者可以结合使用,先实现读写分离,当数据量进一步增长时再考虑分库分表

Q7: 如何选择合适的负载均衡算法?

A7: 根据实际情况选择:

  • 轮询算法:简单易用,适用于从库性能相近的场景
  • 随机算法:实现简单,负载分布均匀
  • 权重算法:根据从库性能设置不同权重,适用于从库性能差异较大的场景
  • 最少连接数算法:根据当前连接数动态分配请求,适用于请求处理时间差异较大的场景

Q8: 如何处理主库故障?

A8: 主库故障处理流程:

  1. 检测主库故障:通过健康检查机制检测主库不可用
  2. 选择新主库:从从库中选择一个合适的实例作为新主库
  3. 提升从库为主库
    sql
    STOP SLAVE;
    RESET MASTER;
    SET GLOBAL read_only = 0;
  4. 重新配置复制关系:其他从库重新指向新主库
    sql
    CHANGE MASTER TO MASTER_HOST='new_master_host', MASTER_USER='replication_user', MASTER_PASSWORD='password', MASTER_AUTO_POSITION=1;
    START SLAVE;
  5. 切换应用数据源:将应用的写操作指向新主库
  6. 恢复服务:验证系统正常运行后恢复服务