MySQL主从库的读操作有几种分配策略?

面试场景

面试官:”你们的数据库做了读写分离吗?读操作怎么分配的?”

读写分离是数据库高可用的基础方案,但”从库读”存在数据延迟问题,需要有策略应对。


读写分离基础

架构图

       ┌────────────────┐
       │   应用服务      │
       └───────┬────────┘
               │
       ┌───────┴────────┐
       │  读写分离代理   │
       └───────┬────────┘
               │
       ┌───────┴────────────────┐
       │                        │
  ┌────┴────┐           ┌──────┴──────┐
  │  主库   │ ─同步复制→ │  从库1/从库2│
  │ (写)    │           │   (读)      │
  └─────────┘           └─────────────┘

核心问题

主从复制存在延迟,从库读到的可能是旧数据。

时间线:
T1: 主库写入数据
T2: 用户查询从库 → 数据还没同步过来!
T3: 从库同步完成

策略一:强制读主库

适用场景

对数据实时性要求极高的查询。

实现方式

@Service
public class OrderService {
    
    @Autowired
    @Qualifier("masterDataSource")
    private DataSource masterDataSource;
    
    // 订单创建后立即查询,走主库
    @Transactional
    public OrderVO createAndQuery(OrderRequest request) {
        Order order = createOrder(request);
        
        // 使用主库数据源查询
        return queryFromMaster(order.getId());
    }
}

注解方式

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ForceMaster {
}

// 拦截器切换数据源
@Aspect
@Component
public class DataSourceAspect {
    
    @Around("@annotation(ForceMaster)")
    public Object forceMaster(ProceedingJoinPoint pjp) throws Throwable {
        DynamicDataSource.setMaster();
        try {
            return pjp.proceed();
        } finally {
            DynamicDataSource.clear();
        }
    }
}

// 使用
@ForceMaster
public Order getOrder(Long orderId) {
    return orderMapper.findById(orderId);
}

策略二:延迟检测

原理

查询从库前,检查主从延迟是否在可接受范围内。

实现方式

public Order getOrder(Long orderId) {
    // 检查从库延迟
    long lag = getReplicationLag();
    
    if (lag > 1000) {  // 延迟超过1秒
        return queryFromMaster(orderId);
    } else {
        return queryFromSlave(orderId);
    }
}

private long getReplicationLag() {
    // 查询从库状态
    // SHOW SLAVE STATUS
    Map<String, Object> status = jdbcTemplate.queryForMap("SHOW SLAVE STATUS");
    return (Long) status.get("Seconds_Behind_Master");
}

缺点

  • 每次查询都要检测延迟,有性能开销
  • 延迟值不完全准确

策略三:写后强制主库时间窗口

原理

写操作后N秒内,该用户的读操作都走主库。

实现方式

@Service
public class OrderService {
    
    private static final String WRITE_FLAG = "user:write:flag:";
    
    @Autowired
    private RedisTemplate<String, String> redis;
    
    public Order createOrder(OrderRequest request) {
        Order order = doCreate(request);
        
        // 标记该用户刚执行过写操作,5秒内读走主库
        redis.opsForValue().set(
            WRITE_FLAG + request.getUserId(), 
            "1", 
            5, TimeUnit.SECONDS
        );
        
        return order;
    }
    
    public Order getOrder(Long userId, Long orderId) {
        // 检查是否有写标记
        String flag = redis.opsForValue().get(WRITE_FLAG + userId);
        
        if ("1".equals(flag)) {
            return queryFromMaster(orderId);
        } else {
            return queryFromSlave(orderId);
        }
    }
}

策略四:半同步复制

原理

主库等待至少一个从库确认收到binlog后才返回。

MySQL配置

-- 主库
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 1000;  -- 1秒超时

-- 从库
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

优缺点

优点 缺点
数据强一致 写性能下降
无需应用层控制 网络抖动影响可用性

策略五:MGR组复制

原理

MySQL Group Replication,多主模式,Paxos协议保证一致性。

┌──────────────────────────────────────┐
│            MGR 集群                   │
├──────────────────────────────────────┤
│  节点1 ←→ 节点2 ←→ 节点3              │
│  (主)     (主)     (主)              │
│  读写     读写     读写              │
└──────────────────────────────────────┘

特点

  • 任意节点都可读写
  • 数据强一致
  • 自动故障切换

策略对比

策略 一致性 性能 复杂度 适用场景
强制主库 关键查询
延迟检测 通用
写后窗口 常见选择
半同步 金融级
MGR 新项目

中间件支持

ShardingSphere读写分离

spring:
  shardingsphere:
    rules:
      readwrite-splitting:
        data-sources:
          rw:
            write-data-source-name: master
            read-data-source-names:
              - slave1
              - slave2
            load-balancer-name: round-robin
        load-balancers:
          round-robin:
            type: ROUND_ROBIN

Hint强制主库

// ShardingSphere Hint
try (HintManager hintManager = HintManager.getInstance()) {
    hintManager.setWriteRouteOnly();  // 强制走主库
    return orderMapper.findById(orderId);
}

面试答题框架

读写分离问题:主从延迟导致读到旧数据

解决策略:
1. 强制主库:关键查询走主库
2. 延迟检测:延迟大时切主库
3. 写后窗口:写操作后N秒读主库
4. 半同步复制:等从库确认
5. MGR组复制:强一致集群

常用方案:
- 业务层:自定义@ForceMaster注解
- 中间件:ShardingSphere读写分离
- 数据库:半同步/MGR

总结

场景 推荐策略
写后立即读 强制主库/写后窗口
普通查询 从库 + 延迟检测
金融级业务 半同步/MGR
报表查询 从库(允许延迟)