读写分离 - 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. 数据同步完成 ✓复制延迟问题
原因:
- 主库压力大: Binlog生成速度快于从库回放速度
- 从库硬件差: CPU/磁盘性能不足
- 大事务: Master执行快,Slave回放慢
- 网络延迟: 跨机房复制
监控延迟:
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: 初始版本,讲解读写分离架构与实践