核心概念
读写分离是将数据库的读操作和写操作分离到不同数据库实例的技术,用于提升数据库并发能力。
读写分离架构
典型架构:
应用层
↓
读写分离中间件
↓ ↓
主库 从库 1
从库 2
从库 3
写操作 → 主库
读操作 → 从库
读写分离优势
| 优势 | 说明 |
|---|---|
| 提升并发 | 读操作分散到多个从库 |
| 性能提升 | 主库专注于写操作 |
| 高可用 | 从库可作为备用 |
| 扩展性 | 可动态添加从库 |
读写分离挑战
| 挑战 | 说明 | 解决方案 |
|---|---|---|
| 数据一致性 | 从库延迟导致读到旧数据 | 强制读主库 |
| 事务处理 | 事务内读写分离复杂 | 事务内固定路由 |
| 故障切换 | 主库故障处理复杂 | 自动故障转移 |
读写分离实现方案
方案 1:应用层路由
原理: 应用层根据 SQL 类型路由到不同数据源。
// 数据源路由
public class DataSourceRouter {
private static final ThreadLocal<DataSourceType> CONTEXT
= new ThreadLocal<>();
public enum DataSourceType {
MASTER, // 主库
SLAVE_1, // 从库 1
SLAVE_2 // 从库 2
}
// 设置数据源
public static void useMaster() {
CONTEXT.set(DataSourceType.MASTER);
}
public static void useSlave() {
CONTEXT.set(DataSourceType.SLAVE_1); // 简单轮询
}
public static DataSourceType get() {
return CONTEXT.get();
}
}
// AOP 拦截
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(Master)")
public void useMaster() {
DataSourceRouter.useMaster();
}
@Before("@annotation(Slave)")
public void useSlave() {
DataSourceRouter.useSlave();
}
}
// 使用
@Service
public class OrderService {
@Master // 写操作
public void createOrder(Order order) {
orderMapper.insert(order);
}
@Slave // 读操作
public Order getOrder(Long id) {
return orderMapper.selectById(id);
}
}
优点:
- 灵活可控
- 无需额外组件
缺点:
- 代码侵入性强
- 维护成本高
方案 2:ShardingSphere-JDBC
原理: JDBC 层中间件,透明读写分离。
<!-- Maven 依赖 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>5.3.0</version>
</dependency>
# application.yml 配置
spring:
shardingsphere:
datasource:
names: ds-master,ds-slave-0,ds-slave-1
ds-master:
jdbc-url: jdbc:mysql://master:3306/order_db
username: root
password: password
ds-slave-0:
jdbc-url: jdbc:mysql://slave1:3306/order_db
username: root
password: password
ds-slave-1:
jdbc-url: jdbc:mysql://slave2:3306/order_db
username: root
password: password
rules:
readwrite-splitting:
data-groups:
rw-group:
write-data-source-name: ds-master
read-data-source-names: ds-slave-0,ds-slave-1
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
优点:
- 透明路由
- 配置简单
- 支持负载均衡
缺点:
- 增加 JDBC 层开销
- 调试复杂
方案 3:ProxySQL
原理: MySQL 代理层,自动读写分离。
-- ProxySQL 配置
-- 1. 配置主从服务器
INSERT INTO mysql_servers (hostgroup_id, hostname, port)
VALUES (1, 'master', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port)
VALUES (2, 'slave1', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port)
VALUES (2, 'slave2', 3306);
-- 2. 配置读写分离规则
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES
(1, 1, '^SELECT.*FOR UPDATE$', 1, 1), -- 写查询到主库
(2, 1, '^SELECT', 2, 1); -- 读查询到从库
-- 3. 加载配置
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;
优点:
- 应用无感知
- 高性能
- 支持故障转移
缺点:
- 需要额外部署
- 配置复杂
方案 4:MyCat
原理: 数据库中间件,支持读写分离和分库分表。
<!-- schema.xml 配置 -->
<schema name="order_db" checkSQLschema="false" sqlMaxLimit="100">
<table name="orders" dataNode="dn_master" type="global"/>
</schema>
<dataNode name="dn_master" dataHost="master" database="order_db"/>
<dataNode name="dn_slave1" dataHost="slave1" database="order_db"/>
<dataNode name="dn_slave2" dataHost="slave2" database="order_db"/>
<dataHost name="master" maxCon="1000" minCon="10" balance="1" writeType="0">
<heartbeat>select user()</heartbeat>
<writeHost host="master" url="master:3306" user="root" password="password"/>
</dataHost>
<dataHost name="slave1" maxCon="1000" minCon="10" balance="1">
<heartbeat>select user()</heartbeat>
<readHost host="slave1" url="slave1:3306" user="root" password="password"/>
</dataHost>
优点:
- 功能强大
- 支持分库分表
缺点:
- 学习成本高
- 运维复杂
数据一致性问题
问题 1:主从延迟
场景:
1. 用户下单(写主库)
2. 立即查询订单(读从库)
3. 从库延迟,查询失败
解决方案 1:强制读主库
// 关键业务强制读主库
@Master
public Order getOrderAfterCreate(Long orderId) {
return orderMapper.selectById(orderId);
}
// 或标记关键数据
public Order getOrder(Long orderId, boolean forceMaster) {
if (forceMaster) {
DataSourceRouter.useMaster();
}
return orderMapper.selectById(orderId);
}
解决方案 2:等待从库追上
public Order getOrder(Long orderId) {
// 检查从库延迟
long delay = replicationMonitor.getDelay();
if (delay > 1000) { // 延迟超过 1 秒
DataSourceRouter.useMaster();
}
return orderMapper.selectById(orderId);
}
解决方案 3:缓存最新数据
public Order getOrder(Long orderId) {
// 先查缓存
Order order = cache.get(orderId);
if (order != null) {
return order;
}
// 缓存未命中,查从库
order = orderMapper.selectById(orderId);
if (order != null) {
cache.set(orderId, order, 60); // 缓存 60 秒
}
return order;
}
问题 2:事务内读写分离
// 错误:事务内切换数据源
@Transactional
public void processOrder(Long orderId) {
Order order = orderMapper.selectById(orderId); // 从库
order.setStatus(1);
orderMapper.update(order); // 主库(事务失效)
}
// 正确:事务内固定数据源
@Transactional
@Master // 事务内都使用主库
public void processOrder(Long orderId) {
Order order = orderMapper.selectById(orderId); // 主库
order.setStatus(1);
orderMapper.update(order); // 主库
}
问题 3:全局表一致性
场景:配置表、字典表等全局表需要实时一致
解决方案:
# ShardingSphere 配置全局表
spring:
shardingsphere:
rules:
readwrite-splitting:
tables:
config_table: # 全局表
actual-data-nodes: ds-master
或应用层特殊处理:
// 全局表强制读主库
@Master
public Config getConfig(String key) {
return configMapper.selectByKey(key);
}
负载均衡策略
轮询策略
// Round Robin
public class RoundRobinLoadBalancer {
private AtomicInteger index = new AtomicInteger(0);
public DataSource getDataSource(List<DataSource> dataSources) {
int idx = Math.abs(index.getAndIncrement() % dataSources.size());
return dataSources.get(idx);
}
}
权重策略
// 根据从库性能分配权重
public class WeightLoadBalancer {
static class SlaveWeight {
DataSource dataSource;
int weight; // 权重
int currentWeight; // 当前权重
}
public DataSource getDataSource(List<SlaveWeight> slaves) {
int totalWeight = slaves.stream().mapToInt(s -> s.weight).sum();
int maxWeight = slaves.stream().mapToInt(s -> s.weight).max().getAsInt();
int offset = new Random().nextInt(totalWeight);
int current = 0;
for (SlaveWeight slave : slaves) {
current += slave.weight;
if (offset < current) {
return slave.dataSource;
}
}
return slaves.get(0).dataSource;
}
}
延迟优先策略
// 选择延迟最低的从库
public class LatencyLoadBalancer {
public DataSource getDataSource(List<SlaveInfo> slaves) {
return slaves.stream()
.min(Comparator.comparingLong(SlaveInfo::getDelay))
.map(SlaveInfo::getDataSource)
.orElse(slaves.get(0).getDataSource());
}
}
最佳实践
配置建议
# ShardingSphere 生产配置
spring:
shardingsphere:
props:
sql-show: false # 生产关闭 SQL 日志
executor-size: 32 # 执行器线程数
rules:
readwrite-splitting:
data-groups:
rw-group:
write-data-source-name: ds-master
read-data-source-names: ds-slave-0,ds-slave-1,ds-slave-2
load-balancer-name: weight
load-balancers:
weight:
type: WEIGHT
props:
ds-slave-0: 3
ds-slave-1: 2
ds-slave-2: 1
监控指标
// 读写分离监控
@Component
public class ReadWriteSplittingMonitor {
@Autowired
private DataSource dataSource;
// 监控主从延迟
@Scheduled(fixedRate = 5000)
public void monitorDelay() {
long delay = getReplicationDelay();
metrics.record("replication_delay", delay);
if (delay > 60) {
alert.send("主从延迟超过 60 秒");
}
}
// 监控读写比例
public void recordQuery(String type) {
metrics.increment("db.query." + type);
}
}
参考资料
- ShardingSphere 官方文档
- ProxySQL 官方文档
- 《分布式数据库中间件实践》