MySQL读写分离

MySQL读写分离是一种数据库架构优化方案,通过将读操作和写操作分离到不同的数据库节点,以提高系统性能和可用性。其核心思想是:写操作(INSERT、UPDATE、DELETE)由主库(Master)处理,读操作(SELECT)由从库(Slave)处理,利用主从复制机制保证数据一致性。这种架构能够有效分担单库压力,提升系统的并发处理能力。

一、读写分离的基本概念

1. 读写分离的目的

- 减轻主库压力:将读操作分散到从库,主库专注处理写操作

- 提高查询性能:多个从库可同时处理读请求,提升查询吞吐量

- 增强系统可用性:主库故障时,可快速切换到从库提供读服务

- 灵活扩展:可根据读负载动态增加从库数量

2. 读写分离的前提

- 主从复制:必须先搭建好主从复制环境,确保从库数据与主库一致

- 应用适配:应用程序或中间件需能区分读写操作并路由到相应节点

- 数据一致性容忍:需接受一定程度的数据延迟(主从复制延迟)

3. 读写分离的挑战

- 数据一致性:主从复制存在延迟,可能导致读不到最新数据

- 故障转移:主库或从库故障时,需自动切换以保证服务可用

- 事务处理:跨库事务和写后立即读场景处理复杂

- 负载均衡:需合理分配读请求,避免部分从库负载过高

二、读写分离的架构

1. 一主一从架构

最简单的读写分离架构,一个主库对应一个从库:

- 主库:处理所有写操作和部分关键读操作

- 从库:处理大部分读操作

优点:结构简单,易于维护

缺点:读性能提升有限,从库故障后无备用节点

2. 一主多从架构

一个主库对应多个从库,是最常用的读写分离架构:

- 主库:处理所有写操作

- 多个从库:共同分担读操作,可根据负载动态增减

优点:读性能可线性扩展,可用性高

缺点:主库仍可能成为瓶颈,需合理分配读请求

3. 级联复制架构

主库→从库→从库的级联结构,减轻主库的复制压力:

- 主库:处理写操作,仅向一个从库(中间层)复制数据

- 中间层从库:既作为主库的从库,又作为其他从库的主库

- 下层从库:处理读操作,从中间层从库复制数据

优点:减少主库的IO压力,适合从库数量较多的场景

缺点:数据延迟可能增加,架构较复杂

4. 双主架构

两个主库互为主从,可同时处理读写操作:

- 主库A和主库B互相同步数据

- 可将写操作分配到两个主库,读操作分配到各自的从库

优点:写性能提升,避免单主瓶颈

缺点:需处理数据冲突,实现复杂

三、读写分离的实现方式

1. 应用程序层实现

在应用程序中直接实现读写分离逻辑,根据SQL类型选择连接主库或从库。

(1)实现方式

- 配置多个数据源:主库数据源和从库数据源

- 编写路由逻辑:根据操作类型(读/写)选择相应的数据源

- 结合ORM框架:如MyBatis可通过插件实现,Spring可通过AOP实现

(2)Spring + MyBatis示例
// 1. 配置多数据源
@Configuration
public class DataSourceConfig {
    // 主库数据源
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    
    // 从库数据源
    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    
    // 动态数据源
    @Bean
    public DataSource routingDataSource() {
        Map dataSources = new HashMap<>();
        dataSources.put("master", masterDataSource());
        dataSources.put("slave", slaveDataSource());
        
        DynamicDataSource routingDataSource = new DynamicDataSource();
        routingDataSource.setTargetDataSources(dataSources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource());
        return routingDataSource;
    }
}

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

// 3. 数据源上下文
public class DataSourceContextHolder {
    private static final ThreadLocal contextHolder = new ThreadLocal<>();
    
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }
    
    public static String getDataSourceKey() {
        return contextHolder.get();
    }
    
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }
}

// 4. AOP实现读写路由
@Aspect
@Component
public class DataSourceAspect {
    // 读操作切入点
    @Pointcut("execution(* com.example.dao..*.select*(..)) || " +
              "execution(* com.example.dao..*.get*(..)) || " +
              "execution(* com.example.dao..*.query*(..))")
    public void readPointcut() {}
    
    // 写操作切入点
    @Pointcut("execution(* com.example.dao..*.insert*(..)) || " +
              "execution(* com.example.dao..*.update*(..)) || " +
              "execution(* com.example.dao..*.delete*(..))")
    public void writePointcut() {}
    
    @Before("readPointcut()")
    public void setReadDataSource() {
        DataSourceContextHolder.setDataSourceKey("slave");
    }
    
    @Before("writePointcut()")
    public void setWriteDataSource() {
        DataSourceContextHolder.setDataSourceKey("master");
    }
    
    @After("readPointcut() || writePointcut()")
    public void clearDataSource() {
        DataSourceContextHolder.clearDataSourceKey();
    }
}
(3)优缺点

优点:

- 实现灵活,可根据业务需求定制路由规则

- 无中间件开销,性能较好

- 开发和调试简单

缺点:

- 读写分离逻辑与业务代码耦合

- 从库数量变化时需修改配置和代码

- 难以实现复杂的负载均衡和故障转移

- 跨团队开发时难以统一管理

2. 中间件实现

通过独立的中间件实现读写分离,应用程序只需连接中间件,无需关心后端数据库拓扑。

(1)主流中间件

① MySQL Router(官方):

- 轻量级代理,由MySQL官方提供

- 支持读写分离、故障转移和负载均衡

- 配置简单,与MySQL兼容性好

② ShardingSphere-JDBC:

- 基于JDBC的客户端中间件

- 支持读写分离、分库分表、分布式事务等

- 功能丰富,扩展性强

③ ProxySQL:

- 高性能的MySQL代理服务器

- 支持读写分离、连接池、查询缓存等

- 适合大规模部署,有完善的监控功能

④ MyCat:

- 开源的分布式数据库中间件

- 支持读写分离、分库分表、全局序列号等

- 配置灵活,文档丰富

(2)MySQL Router配置示例
# 配置文件:mysqlrouter.conf
[DEFAULT]
logging_folder = /var/log/mysqlrouter
runtime_folder = /var/run/mysqlrouter
data_folder = /var/lib/mysqlrouter

[logger]
level = INFO

# 主从集群配置
[cluster.default]
router_id = 1
master_addresses = 192.168.1.100:3306  # 主库地址
slave_addresses = 192.168.1.101:3306,192.168.1.102:3306  # 从库地址
mode = read-write  # 读写分离模式
balancing = round_robin  # 负载均衡策略:轮询

# 读写端口配置
[rw-split:default]
bind_address = 0.0.0.0
bind_port = 6446  # 读写端口,自动路由读写操作
read_only_port = 6447  # 只读端口,强制路由到从库

# 账户配置
[metadata_cache:default]
user = router_user
password = router_password
ttl = 300

应用程序连接方式:

// 连接MySQL Router的读写端口
jdbc:mysql://192.168.1.105:6446/dbname

// 或连接只读端口(强制读操作)
jdbc:mysql://192.168.1.105:6447/dbname
(3)优缺点

优点:

- 读写分离逻辑与业务代码解耦

- 支持复杂的负载均衡策略

- 便于统一管理和动态调整

- 支持自动故障转移

缺点:

- 引入中间件增加系统复杂度

- 存在一定的性能开销(代理模式)

- 学习和配置成本较高

- 中间件本身可能成为瓶颈或单点故障

四、读写分离的关键问题

1. 数据一致性问题

由于主从复制存在延迟,写操作后立即读可能读取不到最新数据,解决方案包括:

① 强制读主库:

对一致性要求高的场景,写操作后强制从主库读取

// 伪代码:写后读主库
@Transactional
public void createOrder(Order order) {
    // 写操作(自动路由到主库)
    orderDAO.insert(order);
    
    // 强制读主库
    DataSourceContextHolder.setDataSourceKey("master");
    Order newOrder = orderDAO.selectById(order.getId());
    DataSourceContextHolder.clearDataSourceKey();
    
    return newOrder;
}

② 延迟读取:

写操作后等待一段时间再读,适用于对实时性要求不高的场景

// 伪代码:延迟读取
public Order getOrderAfterCreate(Long orderId) {
    // 等待主从复制完成(不精确,不推荐在生产环境使用)
    try {
        Thread.sleep(500);  // 等待500ms
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return orderDAO.selectById(orderId);  // 从从库读取
}

③ 监控复制延迟:

通过监控Seconds_Behind_Master值,动态选择从库

// 伪代码:根据延迟选择从库
public DataSource selectSlaveDataSource() {
    List slaves = slaveMonitor.getSlaveInfos();
    // 选择延迟最小的从库
    return slaves.stream()
        .filter(slave -> slave.getSecondsBehindMaster() < 1)  // 延迟小于1秒
        .min(Comparator.comparingInt(SlaveInfo::getSecondsBehindMaster))
        .map(SlaveInfo::getDataSource)
        .orElse(getMasterDataSource());  // 无合适从库则读主库
}

④ 采用半同步复制:

确保主库事务提交前,至少有一个从库已接收日志,减少数据丢失风险

2. 负载均衡策略

多个从库时,需合理分配读请求,常用策略:

① 轮询(Round Robin):

依次将请求分配给每个从库,简单公平,适用于各从库性能相近的场景

② 权重(Weighted):

为性能不同的从库分配不同权重,性能好的从库处理更多请求

# MyCat配置示例

    
        
        
    

③ 最少连接数(Least Connections):

将请求分配给当前连接数最少的从库,动态适应负载变化

④ 哈希(Hash):

根据用户ID或其他关键字哈希,确保同一用户的请求路由到同一从库,利用缓存

⑤ 性能感知(Performance-aware):

根据从库的实时负载(CPU、内存、IO)动态分配请求,需监控支持

3. 故障转移机制

主库或从库故障时,需自动切换以保证服务可用性:

① 从库故障转移:

- 中间件定期检测从库健康状态

- 发现故障从库后,自动将请求路由到其他正常从库

- 从库恢复后,自动重新加入集群

② 主库故障转移:

- 检测到主库故障后,从从库中选举新主库(通常选择数据最新的从库)

- 其他从库重新指向新主库

- 应用程序写请求切换到新主库

- 常用工具:MHA(Master High Availability)、Orchestrator等

4. 事务处理

读写分离环境下的事务处理需特别注意:

① 同一事务中的读写操作应路由到同一节点(通常是主库)

② 避免在事务中混用读写操作,尽量将读操作放在事务外

③ 分布式事务需使用专门的解决方案(如2PC、TCC等)

// 推荐做法:事务中只包含写操作
@Transactional
public Long createOrder(Order order) {
    // 只包含写操作
    orderDAO.insert(order);
    orderItemDAO.batchInsert(order.getItems());
    return order.getId();
}

// 读操作在事务外执行
public OrderDetailVO getOrderDetail(Long orderId) {
    Order order = orderDAO.selectById(orderId);  // 读从库
    List items = orderItemDAO.selectByOrderId(orderId);  // 读从库
    // 组装结果
    return new OrderDetailVO(order, items);
}

五、读写分离的最佳实践

1. 架构设计

- 从库数量根据读负载确定,通常3-5个从库可满足大部分场景

- 主库和从库硬件配置不同:主库侧重写入性能(高IOPS),从库侧重读取性能(大内存)

- 重要业务可采用双主架构,避免单主故障导致写服务不可用

- 跨机房部署时,尽量将应用与数据库部署在同一机房,减少网络延迟

2. 数据一致性保障

- 明确业务对数据一致性的要求,选择合适的解决方案

- 对核心业务(如支付、订单创建)采用读主库策略

- 非核心业务(如商品列表、历史记录)可接受一定延迟,使用从库

- 启用半同步复制,减少数据丢失风险

- 监控主从复制延迟,设置阈值告警(如延迟超过5秒)

3. 应用开发

- 避免使用SELECT ... FOR UPDATE等会在从库执行失败的语句

- 写操作后需立即读取的场景,明确指定读主库

- 避免长事务,减少主库锁竞争和复制延迟

- 分页查询和统计查询尽量在从库执行

- 合理使用缓存,减少数据库读压力

4. 监控与运维

- 监控主从复制状态:Slave_IO_Running、Slave_SQL_Running、Seconds_Behind_Master

- 监控各节点的性能指标:CPU、内存、磁盘IO、连接数、QPS等

- 监控读写分离中间件的状态和性能

- 定期进行故障演练,测试故障转移机制

- 从库定期重建,避免长期运行导致的数据不一致

5. 性能优化

- 从库启用查询缓存(注意MySQL 8.0已移除查询缓存)

- 从库可适当调整参数优化读性能(如增大innodb_buffer_pool_size)

- 主库和从库使用不同的索引策略:主库优化写性能,从库优化读性能

- 大表查询优先在从库执行,并做好查询优化

- 避免在从库执行耗时的操作,防止影响复制进程

6. 安全考虑

- 从库设置为只读(read_only=1),防止误写入

- 复制用户只授予必要的权限(REPLICATION SLAVE)

- 数据库连接密码加密存储,避免明文配置

- 定期备份主库和从库数据,确保可恢复性

六、读写分离与分库分表的结合

在大规模系统中,读写分离通常与分库分表结合使用,形成更强大的分布式数据库架构:

1. 先进行分库分表,按业务拆分数据

2. 每个分库再配置主从复制,实现读写分离

3. 通过中间件(如ShardingSphere)统一管理,提供透明访问

这种架构的优势:

- 同时解决数据量过大和并发过高的问题

- 可根据不同业务模块的特点,定制读写策略

- 系统扩展性更好,可独立扩展某个业务模块的数据库

实现示例:

// ShardingSphere同时配置分库分表和读写分离
spring:
  shardingsphere:
    datasource:
      names: master0, slave0, master1, slave1
      # 主库0配置
      master0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/master0
        username: root
        password: 123456
      # 从库0配置
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3307/slave0
        username: root
        password: 123456
      # 主库1配置
      master1:
        # ... 类似配置
      # 从库1配置
      slave1:
        # ... 类似配置
    rules:
      # 读写分离配置
      readwrite-splitting:
        data-sources:
          ds0:
            type: Static
            props:
              write-data-source-name: master0
              read-data-source-names: slave0
              load-balancer-name: round_robin
          ds1:
            type: Static
            props:
              write-data-source-name: master1
              read-data-source-names: slave1
              load-balancer-name: round_robin
      # 分库分表配置
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..1}.t_order${0..3}
            # ... 分库分表规则配置
        # ... 分片算法配置
    props:
      sql-show: true
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值