
一、为什么需要读写分离当单库的读请求量远超写入量时数据库往往先被读流量压垮。读写分离的核心思路是把写操作集中到主库把读操作分散到多个从库通过主从复制保证数据最终一致。它带来的收益很直接读吞吐可以横向扩展写路径保持单一入口故障时还能快速切换。但工程上真正难的不是“分”而是“分得可控”。下面从数据源路由、注解驱动、上下文传递、主从延迟处理四个层面给出可落地的代码设计。二、数据源路由AbstractRoutingDataSourceSpring 提供了AbstractRoutingDataSource它允许在运行时根据一个 key 动态选择真实数据源。我们只需要实现determineCurrentLookupKey方法。public class RoutingDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DataSourceContext.get(); } }这里的DataSourceContext用ThreadLocal保存当前线程应使用的数据源标识避免多线程之间互相污染。public final class DataSourceContext { private static final ThreadLocalString HOLDER new ThreadLocal(); public static void set(String key) { HOLDER.set(key); } public static String get() { return HOLDER.get(); } public static void clear() { HOLDER.remove(); } }三、注解驱动让业务代码无感知如果每个方法都手动set和clear代码会非常啰嗦。更优雅的方式是定义一个注解配合 AOP 在方法执行前后自动切换数据源。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface ReadOnly { String value() default slave; }切面负责在进入方法前写入上下文在方法结束后清理保证线程复用时不残留脏值。Aspect Component public class ReadOnlyAspect { Around(annotation(readOnly)) public Object around(ProceedingJoinPoint pjp, ReadOnly readOnly) throws Throwable { String previous DataSourceContext.get(); DataSourceContext.set(readOnly.value()); try { return pjp.proceed(); } finally { if (previous null) { DataSourceContext.clear(); } else { DataSourceContext.set(previous); } } } }注意这里保存了previous因为读方法内部可能再调用另一个读方法嵌套场景下必须能正确还原而不是简单清空。四、主从延迟强制走主库的兜底主从复制是异步的写完立刻读从库可能读到旧数据。常见做法是在写操作后的一小段时间内强制读主库或者对一致性要求高的查询显式指定主库。Service public class OrderService { ReadOnly(master) public Order getAfterCreate(Long orderId) { return orderMapper.selectById(orderId); } }更通用的方案是结合事务状态判断当前线程处于写事务中时所有读请求都路由到主库避免事务内读到未提交或未同步的数据。public class TransactionAwareKeyResolver { public static String resolve() { if (TransactionSynchronizationManager.isActualTransactionActive()) { return master; } return DataSourceContext.get() null ? slave : DataSourceContext.get(); } }五、配置与注意事项数据源配置需要把主库和从库都注册进去并指定默认目标数据源。从库通常配置多个配合轮询或权重策略做负载均衡。Configuration public class DataSourceConfig { Bean public DataSource routingDataSource( Qualifier(master) DataSource master, Qualifier(slave1) DataSource slave1, Qualifier(slave2) DataSource slave2) { MapObject, Object targets new HashMap(); targets.put(master, master); targets.put(slave, new LoadBalancedDataSource(slave1, slave2)); RoutingDataSource routing new RoutingDataSource(); routing.setTargetDataSources(targets); routing.setDefaultTargetDataSource(master); return routing; } }落地时还要关注三点一是事务边界Transactional内不要混用读写数据源二是连接池隔离主从各自独立配置避免慢查询拖垮写路径三是监控告警对主从延迟、从库连接数做实时观测延迟超过阈值时自动降级到主库。读写分离不是银弹它用一致性换取了吞吐。把路由、注解、上下文和延迟兜底这四层设计清楚代码才能既简洁又可靠。