Skip to content

读写分离 - Read-Write Splitting详解 ​

定义 ​

读写分离 (Read-Write Splitting) 是一种数据库架构优化模式,它将数据库的读操作和写操作分离到不同的服务器实例上执行。通常由一个主库(Master)处理所有的写操作(INSERT/UPDATE/DELETE),多个从库(Slave)处理读操作(SELECT),通过主从复制(Replication)机制保持数据同步。

核心特征 ​

特征说明
一主多从1个Master + N个Slave
写入集中所有写操作只在Master执行
读取分散读操作负载均衡到多个Slave
异步复制Master→Slave通过Binlog异步同步
最终一致性Slave可能存在短暂延迟

架构图 ​

应用层
  ↓
┌─────────────────┐
│  读写分离中间件 │ ← MyCat / ShardingSphere / ProxySQL
└─────────────────┘
  ↓        ↓
  写请求      读请求
  ↓        ↓
┌─────────┐  ┌──────────┬──────────┬──────────┐
│ Master  │─────→│ Slave 1  │ Slave 2  │ Slave 3  │
│ (主库) │ Binlog│ (从库) │ (从库) │ (从库) │
└─────────┘  └──────────┴──────────┴──────────┘
   ↑       ↑     ↑     ↑
  写操作      读操作   读操作   读操作
  
• Master处理: INSERT, UPDATE, DELETE
• Slaves处理: SELECT (负载均衡)
• 复制延迟: 通常 < 1秒

为什么需要读写分离? ​

问题1: 读多写少的负载不均 ​

典型业务场景:

电商平台:
- 商品浏览: 100,000 QPS (读)
- 下单购买: 1,000 TPS (写)
- 读写比: 100:1

社交网络:
- Feed流刷新: 500,000 QPS (读)
- 发布动态: 5,000 TPS (写)
- 读写比: 100:1

新闻门户:
- 文章阅读: 1,000,000 QPS (读)
- 内容发布: 100 TPS (写)
- 读写比: 10000:1

单库瓶颈:

单实例MySQL能力:
- 读QPS: 约5000~10000 (取决于查询复杂度)
- 写TPS: 约1000~3000

如果读写混合:
- 总QPS上限: 约8000
- 无法满足10万+ QPS需求

解决方案:
- 1主 + 9从 = 10个实例
- 写: Master (1000 TPS足够)
- 读: 9个Slave (9 × 8000 = 72,000 QPS)
- 成本增加10倍,读能力提升7倍

问题2: 锁竞争影响读性能 ​

场景:

sql
-- 单库场景
-- 事务A更新热门商品库存(持有行锁)
UPDATE products SET stock = stock - 1 WHERE id = 100;

-- 事务B查询同一商品(被阻塞!)
SELECT * FROM products WHERE id = 100;
-- 等待锁释放...

-- 事务C也要查询(也被阻塞!)
SELECT name, price FROM products WHERE id = 100;
-- 等待锁释放...

读写分离后:

sql
-- Master处理写
UPDATE products SET stock = stock - 1 WHERE id = 100;
-- 持有锁,但不影响读

-- Slave处理读(无锁!)
SELECT * FROM products WHERE id = 100;
-- 从Slave读取,不受Master锁影响 ✓

-- 注意: Slave可能有短暂延迟(通常<1秒)

实现方案 ​

方案1: 应用层控制 ​

Spring Boot + AbstractRoutingDataSource:

java
@Configuration
public class DataSourceConfig {
  
  @Bean
  @ConfigurationProperties("spring.datasource.master")
  public DataSource masterDataSource() {
    return DataSourceBuilder.create().build();
  }
  
  @Bean
  @ConfigurationProperties("spring.datasource.slave")
  public DataSource slaveDataSource() {
    return DataSourceBuilder.create().build();
  }
  
  @Bean
  public DataSource routingDataSource() {
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put("MASTER", masterDataSource());
    targetDataSources.put("SLAVE", slaveDataSource());
    
    RoutingDataSource routingDataSource = new RoutingDataSource();
    routingDataSource.setDefaultTargetDataSource(masterDataSource());
    routingDataSource.setTargetDataSources(targetDataSources);
    return routingDataSource;
  }
}

public class RoutingDataSource extends AbstractRoutingDataSource {
  @Override
  protected Object determineCurrentLookupKey() {
    // 从ThreadLocal获取数据源标识
    return DataSourceContextHolder.getDataSourceType();
  }
}

// AOP拦截器
@Aspect
@Component
public class DataSourceAspect {
  
  @Before("@annotation(readOnly)")
  public void setReadOnlyDataSource(ReadOnly readOnly) {
    DataSourceContextHolder.setDataSourceType("SLAVE");
  }
  
  @Before("@annotation(modify)")
  public void setWriteDataSource(Modify modify) {
    DataSourceContextHolder.setDataSourceType("MASTER");
  }
}

// 使用示例
@Service
public class ProductService {
  
  @Autowired
  private ProductMapper productMapper;
  
  // 自动路由到Slave
  @ReadOnly
  public Product getProduct(Long id) {
    return productMapper.selectById(id);
  }
  
  // 自动路由到Master
  @Modify
  public void updateStock(Long id, Integer quantity) {
    productMapper.updateStock(id, quantity);
  }
}

方案2: 中间件代理 ​

ShardingSphere-Proxy配置:

yaml
# config-sharding.yaml
schemaName: sharding_db

dataSources:
  master_ds:
  url: jdbc:mysql://master:3306/products
  username: root
  password: password
  connectionTimeoutMilliseconds: 30000
  
  slave_ds_0:
  url: jdbc:mysql://slave1:3306/products
  username: root
  password: password
  
  slave_ds_1:
  url: jdbc:mysql://slave2:3306/products
  username: root
  password: password

rules:
- !READWRITE_SPLITTING
  dataSources:
  pr_ds:
  writeDataSourceName: master_ds
  readDataSourceNames:
    - slave_ds_0
    - slave_ds_1
  loadBalancerName: round_robin
  
  loadBalancers:
  round_robin:
  type: ROUND_ROBIN

应用层无需修改,直接连接Proxy:

java
// 正常编写代码,无需关心读写分离
@Autowired
private JdbcTemplate jdbcTemplate;

public List<Product> listProducts() {
  // 自动路由到Slave
  return jdbcTemplate.query("SELECT * FROM products", ...);
}

public void createProduct(Product p) {
  // 自动路由到Master
  jdbcTemplate.update("INSERT INTO products ...", ...);
}

主从复制原理 ​

Binlog复制流程 ​

Master节点          Slave节点
━━━━━━━━━━━━━━       ━━━━━━━━━━━━━━

1. 客户端执行写操作
 UPDATE products SET stock=99 
 WHERE id=100;
 
2. 写入数据文件       
 
3. 记录Binlog         
 ┌──────────────────┐
 │ Event: UPDATE  │
 │ LSN: 12345   │
 │ Data: id=100,  │
 │    stock=99  │
 └──────────────────┘
 
4. 返回客户端成功        

            5. I/O线程请求Binlog
             Request LSN > 12340
             
            6. 接收Binlog事件
             ┌──────────────┐
             │ Relay Log  │
             │ LSN: 12345 │
             └──────────────┘
             
            7. SQL线程重放事件
             UPDATE products 
             SET stock=99 
             WHERE id=100;
             
            8. 数据同步完成 ✓

复制延迟问题 ​

原因:

  1. 主库压力大: Binlog生成速度快于从库回放速度
  2. 从库硬件差: CPU/磁盘性能不足
  3. 大事务: Master执行快,Slave回放慢
  4. 网络延迟: 跨机房复制

监控延迟:

sql
-- 在Slave上执行
SHOW SLAVE STATUS\G

-- 关键字段:
Seconds_Behind_Master: 2  -- 延迟2秒
Slave_IO_Running: Yes   -- I/O线程正常
Slave_SQL_Running: Yes  -- SQL线程正常

解决方案:

sql
-- 1. 并行复制(MySQL 5.7+)
SET GLOBAL slave_parallel_workers = 8;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';

-- 2. 优化主从事务
-- 避免大事务,分批提交

-- 3. 关键查询强制走主库
-- /* FORCE_MASTER */ SELECT * FROM orders WHERE id = ?;

最佳实践 ​

1. 强制主库查询场景 ​

python
def get_order_after_create(order_id):
  """下单后立即查询,必须走主库"""
  
  # 刚创建的订单可能还未复制到Slave
  # 强制从Master读取,保证一致性
  return db.execute(
    "/* FORCE_MASTER */ SELECT * FROM orders WHERE id = %s",
    (order_id,)
  )

# 其他必须走主库的场景:
# - 支付成功后查询订单
# - 修改个人资料后立即查看
# - 秒杀成功后确认订单

2. 读写分离配置建议 ​

yaml
# 生产环境推荐
master:
  instance_count: 1
  specs: 16核32G SSD
  
slaves:
  instance_count: 3-9  # 根据读负载调整
  specs: 8核16G SSD
  
replication:
  mode: semi_sync  # 半同步复制(平衡性能与一致性)
  parallel_workers: 8  # 并行复制线程数
  
load_balance:
  strategy: round_robin  # 轮询
  health_check: 10s  # 健康检查间隔

3. 缓存配合使用 ​

python
import redis

class ProductService:
  def __init__(self):
    self.cache = redis.Redis()
  
  def get_product(self, product_id):
    """三级缓存策略"""
    
    # L1: Redis缓存(最快)
    cached = self.cache.get(f"product:{product_id}")
    if cached:
    return json.loads(cached)
    
    # L2: Slave数据库(次之)
    product = db_slave.query(
    "SELECT * FROM products WHERE id = %s",
    (product_id,)
    )
    
    if product:
    # 写入缓存(TTL 1小时)
    self.cache.setex(
      f"product:{product_id}",
      3600,
      json.dumps(product)
    )
    
    return product
  
  def update_product(self, product_id, data):
    """更新时删除缓存"""
    
    # 1. 更新Master
    db_master.update(
    "UPDATE products SET ... WHERE id = %s",
    (product_id,)
    )
    
    # 2. 删除缓存(下次读取时重建)
    self.cache.delete(f"product:{product_id}")

参考资料 ​

相关术语 ​

技术框架 ​


版本历史:

  • 2026-04-12: 初始版本,讲解读写分离架构与实践

Released under MIT License.