尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Spring Boot读写分离实战:基于AbstractRoutingDataSource与AOP轻量实现
简介面向Spring Boot开发者的数据库读写分离实现指南适合需要优化系统并发读性能、降低主库压力的中高级Java工程师。资源为单个PDF文档大小约48KB涵盖从主从库配置到代码层动态路由的完整思路。内容以AbstractRoutingDataSource为核心演示如何通过ReadWriteSplitRoutingDataSource重写determineCurrentLookupKey()实现数据源切换并配合DbContextHolder使用ThreadLocal保存线程内数据库类型确保读写操作各归其位。同时引入AOP与自定义注解ReadOnlyConnection将读操作自动路由至从库并执行完毕后恢复主库减少业务代码侵入。PDF中对核心类、枚举、拦截器及调用示例均有代码级说明也讨论了事务隔离级别与数据一致性要点。资源已有1008人学习适合想快速落地读写分离、并理解底层路由机制的开发者参考。1. 读写分离在 Spring Boot 里到底解决什么问题Spring Boot 项目的数据库读写分离不是架构师拿来炫技的设计而是业务流量上来之后最直接的一根救命稻草单库的写入压力还在可控范围但读流量一多连接池先被打满主库 CPU 先到 80%慢查询成片出现。此时 MySQL 的主从复制已经在 DBA 那边搭好了代码却还是只连主库从库白白空转。你需要的不是一个重型的分布式中间件而是在 Spring Boot 应用内部把“读请求路由到从库、写请求路由到主库”这件事做干净。本文讲的方案就是基于AbstractRoutingDataSource加 AOP 注解的一套轻量读写分离实现适合中小团队、单体应用、读多写少的业务场景。它不改变 MyBatis、JPA 的用法也不需要引入额外的代理层但能把主库的读压力切实分走。2. 先把主从复制和路由选型理清为什么核心是 AbstractRoutingDataSource2.1 主从复制是前提代码侧只做“路由”这一件事读写分离有一个前置条件MySQL 主从复制已经能正常跑。主库开启 binlog从库通过CHANGE MASTER TO指向主库然后START SLAVE。这一步通常在 DBA 的运维脚本里完成应用侧不需要关心同步细节但要确认一个事实——从库已经持续在拉取主库的 binlog 并回放。从库那边有两个硬性配置建议一是read_only ON防止有人或程序误写从库二是给从库设置独立的server-id不要和主库重复。下面是在从库上做初始化的最简命令通常在 MySQL 命令行里执行CHANGE MASTER TO MASTER_HOST10.0.0.1, MASTER_USERrepl, MASTER_PASSWORDrepl_pass, MASTER_LOG_FILEmysql-bin.000001, MASTER_LOG_POS154; START SLAVE;参数说明MASTER_HOST是主库 IPMASTER_USER和MASTER_PASSWORD是主库上专门给复制用的账号建议只授予REPLICATION SLAVE权限不要用 root。MASTER_LOG_FILE和MASTER_LOG_POS是复制起点用主库的SHOW MASTER STATUS查出来填。执行完START SLAVE后用SHOW SLAVE STATUS\G看Slave_IO_Running和Slave_SQL_Running是否都是Yes。确认主从通之后Spring Boot 这边才谈得上读写分离否则一切路由都是把查询发到一个空库或旧库上。2.2 三种实现方案对比硬编码、路由数据源、中间件“读写分离怎么做”这个问题行业内答案基本可以归成三类按重量级排开方案实现方式优点缺点适用场景双数据源硬编码Service 里注入两个 DataSource查询时手动指定从库思路简单无框架依赖侵入业务代码每个方法都要写判断维护成本高接口少、临时应急AbstractRoutingDataSource AOP 注解运行时按上下文 key 动态选择真实数据源对业务代码侵入小一个注解搞定事务内可控需要自己维护 ThreadLocal 和切面主从延迟需要业务兜底大多数单体应用读多写少中间件 / 代理层ShardingSphere、MyCat 等应用连接代理代理层解析 SQL 并路由功能全、透明支持读写分离 分库分表 强制主库部署和运维成本高引入分布式复杂度分库分表已提上日程、需要强一致路由我一般会选第二种原因很现实中间件方案再强大也要求团队能扛住它的配置和排障成本而第一种双数据源硬编码代码里会散落大量“这个查询该用哪个数据源”的判断改一个接口要翻好几个 Service。AbstractRoutingDataSource是 Spring 自带的抽象类它本身不负责判断只负责按照返回的 key 从targetDataSources里挑一个真实数据源刚好适合做“路由”这件事。结合 AOP就能把数据源切换从业务代码里剥离。2.3 最小配置多数据源配置类与 HikariCP 参数先给出一个能跑的最小配置。application.yml里定义主库和从库两套连接信息spring: datasource: master: jdbc-url: jdbc:mysql://10.0.0.1:3306/shop?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: MasterPool maximum-pool-size: 15 minimum-idle: 5 slave: jdbc-url: jdbc:mysql://10.0.0.2:3306/shop?useSSLfalseserverTimezoneAsia/Shanghai username: shop_slave password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: pool-name: SlavePool maximum-pool-size: 40 minimum-idle: 10 read-only: true参数说明jdbc-url是 Spring Boot 2.x 的写法1.x 用urlmaximum-pool-size是池子最大连接数从库承担更多读流量可以给大一点read-only: true让从库连接直接以只读模式建立从库上误把写操作发过去时MySQL 会直接报错相当于多一层保护。驱动版本建议用com.mysql.cj.jdbc.DriverMySQL 8.0 对应的 JDBC 驱动老版本com.mysql.jdbc.Driver在时区处理上容易埋雷。然后是数据源配置类核心是把主从两个数据源放进路由表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 Primary public DataSource routingDataSource( Qualifier(masterDataSource) DataSource master, Qualifier(slaveDataSource) DataSource slave) { MapObject, Object targetDataSources new HashMap(); targetDataSources.put(DataSourceType.MASTER.name(), master); targetDataSources.put(DataSourceType.SLAVE.name(), slave); RoutingDataSource routing new RoutingDataSource(); routing.setDefaultTargetDataSource(master); routing.setTargetDataSources(targetDataSources); return routing; } }逻辑说明RoutingDataSource继承AbstractRoutingDataSourcedetermineCurrentLookupKey()返回的就是路由 key。默认数据源必须设成主库这样就算开发忘了写注解写操作也不会被错误地送到只读从库上。注意Qualifier注解要写全否则 Spring 在多个 DataSource Bean 存在时不知道注入哪个会直接启动失败。3. 用 DataSource 注解让调用层无感切换AOP 切面的设计与参数3.1 基于 ThreadLocal 的数据源上下文每个线程维护一个路由 key路由 key 必须做到线程隔离否则一个请求设置成从库线程池里另一个请求就跟着串库了。这里用的就是ThreadLocal这是整个读写分离的“状态中枢”。public enum DataSourceType { MASTER, SLAVE } public class DataSourceContextHolder { private static final ThreadLocalDataSourceType CONTEXT new ThreadLocal(); public static void set(DataSourceType dataSourceType) { CONTEXT.set(dataSourceType); } public static DataSourceType get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }逻辑说明set在当前线程上绑定数据源类型get在路由数据源里被调用clear则必须在请求结束或切面执行完之后调用否则线程池复用线程时残留的 key 会污染下一个请求。这个工具类的使用范围不只限于注解切面后面如果要做“根据请求参数强制走主库”这样的个性化路由也可以直接调set。3.2 自定义注解 环绕切面方法级优先默认读从写主有了上下文还需要一个业务入口。自定义一个DataSource注解标注在 Service 方法上切面在方法执行前设好 key方法跑完立刻清理Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DataSource { DataSourceType value() default DataSourceType.SLAVE; }Aspect Component Order(Ordered.LOWEST_PRECEDENCE - 100) public class DataSourceAspect { Around(annotation(dataSource) || within(dataSource)) public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { DataSourceType type dataSource.value(); DataSourceContextHolder.set(type); try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clear(); } } }参数说明Order(Ordered.LOWEST_PRECEDENCE - 100)这个顺序值很关键它保证数据源切面在 Spring 事务管理器创建连接之前执行。如果切面顺序比事务切面晚事务已经通过DataSourceUtils拿好了主库连接路由就形同虚设。within(dataSource)支持把注解打在类上这样类里的所有方法都走同一个数据源方法上的注解优先于类注解。finally里清理 ThreadLocal 是必须写死的习惯否则一旦方法抛异常key 不会被清掉。3.3 Service 里怎么用读方法标从库写方法不标也能走主库有了切面业务代码就干净了。看一个OrderService的典型写法Service public class OrderService { DataSource(DataSourceType.SLAVE) public Order getOrderById(Long id) { return orderMapper.selectById(id); } DataSource(DataSourceType.MASTER) public Long createOrder(Order order) { orderMapper.insert(order); return order.getId(); } }逻辑说明getOrderById显式标注走从库createOrder显式标注走主库。如果某天忘记标注因为路由类的defaultTargetDataSource配的是主库这个方法会落回主库不会出现“写操作跑到从库”这种灾难。这算是一种保守的安全策略。MyBatis 的Mapper接口不需要做任何改动SqlSessionFactory注入的是路由数据源SQL 执行时连接已经按照切面设置好的 key 选定了。注意一个实际使用中的边界同类内部方法调用不会触发 AOP 代理。比如OrderService里一个非事务方法调用了同类的getOrderById()注解不会生效。解决方法是把需要切换数据源的方法拆到另一个 Service 或自注入代理对象。4. 事务、主从延迟与强制走主库读写分离的三个边界条件4.1 Transactional 会让路由失效事务内连接被绑定这是读写分离里最容易翻车的地方。断言一条一旦方法进入 Spring 管理的事务数据源路由就定了事务里再切换注解也换不回连接。原因是事务管理器在开启事务时会从路由数据源拿到一个连接并绑定到当前线程后续所有数据库操作都复用这个连接determineCurrentLookupKey()即使中途变了连接也不会换。所以事务和读写分离的正确配合方式是这样Service public class OrderService { DataSource(DataSourceType.MASTER) Transactional(rollbackFor Exception.class) public void createOrderWithItems(Order order, ListOrderItem items) { orderMapper.insert(order); orderItemMapper.batchInsert(items); } }参数说明rollbackFor Exception.class让所有异常都触发回滚不要只依赖默认的运行时异常否则业务异常比如余额不足的自定义异常提交了事务数据就不一致了。带有Transactional的方法如果内部还调用了只读查询这个查询也会走主库这是正常现象不是 bug。要想读从库就把这个查询方法拆到另一个没有Transactional的 bean 方法里。事务隔离级别和传播行为建议都用默认值读写分离场景下先不用REQUIRES_NEW或NESTED那会让每个子事务重新拿连接连接池压力会被放大。4.2 主从延迟刚写完就查不到强制走主库是兜底方案MySQL 主从复制是异步的主库提交事务、写 binlog到从库回放完中间的延迟正常情况下是毫秒级但压力大或从库配置低时可能几秒。最典型的症状是用户刚下单成功页面跳转后立刻查订单列表结果列表里没有这条新订单。这不是 BUG是延迟。对一致性要求高的业务必须强制走主库。常见做法是再定义一个注解值或DataSourceContextHolder.set(DataSourceType.MASTER)在“下单后马上查订单”的接口里直接把查询指向主库DataSource(DataSourceType.MASTER) public Order getOrderDetailForceMaster(Long orderId) { return orderMapper.selectById(orderId); }参数说明这里没有魔法就是在读方法上显式标注主库。代价是这部分读流量又落回主库所以强制走主库的接口要控制住范围不能整张订单表的所有查询都走主库那读写分离就白做了。按业务优先级分订单详情、支付状态、用户余额这种强一致场景走主库历史订单列表、报表统计、商品浏览这种能容忍秒级延迟的走从库。MySQL 8.0 还支持半同步复制能减少数据丢失风险但解决不了延迟时间本身。4.3 连接池参数怎么调从库池子要比主库大且连接超时要兜底读写分离不是换一个 DataSource 就完事连接池参数得跟着流量结构改。从库承接的读请求数量通常远大于主库的写请求所以从库池子要大主库池子反而要控制住避免写库被一堆空闲连接拖累。参考配置如下spring: datasource: master: hikari: max-lifetime: 1800000 connection-timeout: 30000 validation-timeout: 3000 slave: hikari: read-only: true max-lifetime: 1800000 connection-timeout: 30000 validation-timeout: 3000参数说明max-lifetime建议 30 分钟和 MySQL 的wait_timeout默认 8 小时保持安全距离防止连接被 MySQL 服务端断开后客户端还在用。connection-timeout设 30 秒这是拿连接的等待上限不是悲观锁的超时。read-only对从库一定开连接建立后 JDBC 驱动会以只读模式工作。从库池子大小的经验值先按“从库 QPS 预估峰值 / 单连接每秒能执行的简单查询次数”粗算然后压测调优。常见的误区是从库池子配置 200那只会让数据库连接数爆炸而不是让查询更快。5. 避坑记录读写分离上线后最常踩的 5 个翻车瞬间5.1 路由不生效接口全打了注解日志里却全走主库现象代码里DataSource(DataSourceType.SLAVE)分明写着但从库的监控里一个查询都没有全在主库上跑。原因有两个高频元凶。第一种AOP 切面的Order顺序不对Spring 事务切面先执行并拿到主库连接之后数据源切面再设置 key 已经晚了。第二种注解打在private方法或同类内部调用上Spring AOP 用的是代理模式拿不到目标对象里自调用的方法。解决切面类上明确写Order(Ordered.LOWEST_PRECEDENCE - 100)并且把注解打在public方法上。如果方法必须在类内部自己调用把调用拆到独立的 Service 类或者注入自身代理。验证方法也简单在RoutingDataSource里打印当前 key然后访问一个接口看日志。5.2 连接串库第二个请求莫名其妙写到了从库现象接口 A 标注从库接口 B 不标注应该走主库。压测时发现接口 B 的写操作报错提示连接是只读的或者数据写到了从库。原因ThreadLocal在请求结束后没有清理。Tomcat 的工作线程是复用的当前请求在接口 A 里设置了 SLAVE结束后clear()没执行下一个请求复用这个线程执行接口 B路由 key 还是 SLAVE。解决切面里把clear()放进finally保证不管是正常返回还是抛异常都清理。如果存在绕过切面的代码路径比如定时任务里直接调DataSourceContextHolder.set()要自己包一层 try-finally。这块属于典型的“平时无事压测现形”也是我用血泪经验换来的教训。5.3 从库查不到刚写入的数据主从延迟和 Binlog 断点混合出现现象业务上刚更新一条数据立刻从从库查查到的是旧值过几秒再查又对了。严重的时候从库一直落后几分钟主库 CPU 不高从库却一直追不上。原因主从延迟过高是常态问题需要用SHOW SLAVE STATUS看Seconds_Behind_Master。但更隐蔽的是复制断开了比如从库上执行过写操作导致 SQL 线程报错从库复制停在某个位置后面全是延迟。解决先用SHOW SLAVE STATUS\G确认Slave_SQL_Running是否为 YesSeconds_Behind_Master是否持续为 0。从库保持read_onlyON定期监控复制状态。代码侧把“刚写入数据立刻读”的接口强制走主库同时给从库查询的缓存设置一个合理的过期时间比如本地缓存 3 到 5 秒很多时候延迟被缓存吸收了根本不会让用户感受到。5.4 MyBatis 一级缓存导致切换数据源后读到脏数据现象同一个方法里先查一次主库的某个数据再切到从库查同一条数据结果从库返回的还是主库那次查询的结果。原因MyBatis 的一级缓存默认作用域是 SqlSession在 Spring 管理下一次事务或一次请求内可能复用同一个 SqlSession缓存了查询结果。数据源切了但 SqlSession 没有重建就读到了“上一个数据源”的缓存。解决从库的查询方法不要和主库的写在同一个 SqlSession 生命周期里。最简单的是把读写方法拆到不同 Service 或加flushCache控制。更彻底的做法是全局关掉一级缓存localCacheScopeSTATEMENT但这会影响普通查询性能不推荐一上来就关。先确认你的坑是不是这个再决定动哪一层。5.5 连接池参数配错从库的maximum-pool-size比主库还小现象从库连接池被打满查询超时主库反而空闲。原因不分场景地把一套连接池参数复制给了两个数据源。从库承担全部读流量池子却和主库一样是 10 个必然先撑爆。解决按读流量占比重新分配。从库的池子要大于主库但不要线性放大连接数超过数据库max_connections上限后只会让等待更慢。给一个起步配置主库 10 到 20从库 30 到 60然后压测观察主库和从库的Threads_connected以数据库不出现连接等待为准。压测是要跑真实业务流量或回放录制的 SQL不要用单条查询死压不然连接数上去了CPU 也上去了看不出瓶颈。6. 验证读写分离真正生效一个可落地的检查技巧验证读写分离不是靠肉眼和祈祷我习惯先用日志确认路由 key再从库造一个“探测表”做黑盒验证。在RoutingDataSource里加一段调试日志Override protected DataSource determineTargetDataSource() { DataSourceType key DataSourceContextHolder.getDataSourceType(); if (log.isDebugEnabled()) { log.debug(当前线程: {} 路由数据源: {}, Thread.currentThread().getName(), key); } return super.determineTargetDataSource(); }然后在从库上建一张主库不存在的小表比如t_route_hint。业务里随便写一个只读查询接口去查这张表能查到说明路由到了从库报Table doesnt exist说明路由仍落在主库。这个方法不依赖任何 APM 工具是最快的黑盒验证。压测时可以盯两个指标主库的Threads_running是否下降从库的QPS是否上升。我用 JMeter 或 wrk 做读接口压测同时在 MySQL 里每 5 秒采一次SHOW GLOBAL STATUS里的Questions。如果从库的 QPS 变化不大多半路由没生效回头查第一版配置里的切面顺序问题。读写分离本身不解决慢查询它只解决“读流量把主库拖垮”的问题。上线前我会把路由日志里包路径的 debug 日志开着跑一遍核心链路确认读操作都走 SlaavePool、写操作都走 MasterPool再关掉日志不然线上日志量会压垮磁盘。最后补一句经验如果你的业务读写比例没有明显偏向读或者从库复制经常断这个方案就不要急着上先把主库容量和 SQL 优化做完比任何路由都值钱。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

意大利艺术涂料值不值得做?从材料原理到施工避坑全解析

意大利艺术涂料值不值得做?从材料原理到施工避坑全解析

装修界这几年的风向转得很快,前几年大家还在纠结乳胶漆刷什么颜色、墙纸选什么花纹,现在越来越多的业主设计师一开口就问:意大利艺术涂料到底值不值得做?我在项目里跟艺术涂料打交道也有七八年了,经手过从几百平的大平…

📅 2026/10/9 11:24:45
Java多线程进阶:Thread属性、线程中断与join方法实战排查指南

Java多线程进阶:Thread属性、线程中断与join方法实战排查指南

1. 从线程的“身份档案”说起:属性能告诉我们什么很多人在刚开始接触Java多线程时,习惯把Thread类当成一个“能跑东西的对象”来用:new一个Thread,重写run(),start(),完事。但对Thread本身自带的那一堆属性…

📅 2026/10/9 11:24:45
t3code:面向移动开发者的沙盒调试与真机热更新工具

t3code:面向移动开发者的沙盒调试与真机热更新工具

1. 项目概述:t3code 是什么?它解决的不是“工具问题”,而是开发流程断点t3code 这个名字乍看像某个开源库或小众 CLI 工具,但结合热搜词里高频出现的CLI、Electron、iOS、Android、electron打包apk、ios端ipa签名工具、android/da…

📅 2026/10/9 11:24:45
MORE NEWS

更多资讯

📰

你的OpenClaw会主动干活吗?用Heartbeat+Cron+Webhook把TaoToken接进Agent工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

零刻mini主机/群晖/Macmini 用docker部署OpenClaw喂饭级踩坑详细教程|以及多用户多Agent对接 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Python print渲染8x8点阵字:从取模到终端显示全流程

1. 从一个打印需求说起:点阵字到底能玩出什么花样很多人第一次接触点阵字,是在老式收银机、电子秤或者公交站牌上。那种由一个个小圆点拼出来的数字和字母,远看粗糙,近看却有一种独特的机械美感。后来做嵌入式开发的朋友告诉我&am…

📰

无穷级数求和八大公式详解:从等比级数到阿贝尔定理

1. 从一道让无数人卡壳的题目说起如果你学过高等数学,大概率在某个深夜对着无穷级数的求和题发过呆。题目给一个看着挺规整的式子,要求你算出它收敛到哪个值,你翻了翻课本,发现公式一大堆,但真到用的时候一个都想不起来…

📰

SpringBoot+Vue物流管理系统:从数据库到全流程跑通指南

简介:该物流管理系统基于Java、Spring Boot、Vue与MySQL构建,是一套完整的高分毕业设计项目,面向高校毕业生、课程设计及期末大作业场景,可帮助企业提升订单、库存、运输等环节的管理效率。包内收录项目源码、数据库脚本与开发工具…

📰

红外狗类目标检测数据集实战:从数据准备到YOLO训练与调参

简介:这份红外狗类目标检测数据集面向从事热成像目标检测的算法工程师、农业安防开发者及高校研究人员,用于解决低光照、夜间或恶劣天气下动物识别样本稀缺的问题。数据全部为红外热成像图像,标注采用YOLO格式,包含归一化边界框坐…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬