
1. 整体设计多数据源方案选型与拆解1.1 为什么直接用MyBatis-Plus做多数据源会痛先说个真实场景。我手头这个项目同时连着三个库主业务库、历史库和报表库。最开始想得简单用MyBatis-Plus直接就上了结果第一天就被它“一个SqlSessionFactory绑定一个DataSource”的机制教做人。MyBatis-Plus本身并不提供多数据源能力它默认只是把一个数据源和一套Mapper绑定在一起。你要是硬在代码里声明两个DataSourceBean然后塞给同一个SqlSessionFactory启动时大概率会报“找不到唯一DataSource”或者Mapper根本连不上对应库。后来我尝试手动切数据源在Service层先拿到目标DataSource再用DataSourceUtils打开连接最后注入到MyBatis的执行流程里。这个方案跑通了一个最简单的Demo但一上生产就发现问题——Spring事务会把连接绑定到当前线程一旦Transactional先开启后面再怎么切数据源MyBatis拿到的还是事务里那个旧连接。更麻烦的是如果多个Mapper分别归属于不同库每次都要手动获取SqlSessionTemplate代码变得又长又绕维护成本极高。所以当看到MyBatis-Plus配套的dynamic-datasource-spring-boot-starter时我一下就想通了与其手工造轮子不如把“动态路由”这个职责抽出来。它内部维护一个数据源注册表通过AOP拦截DS注解把当前线程要用的数据源key放到ThreadLocal里然后交给DynamicRoutingDataSource去路由。这个机制像极了快递分拣中心包裹上贴了单号分拣机器根据单号把包裹送到不同货区不需要人工一个个搬。多数据源难题的核心就是把这套“路由逻辑”做对、做稳。另外也顺便对比一下JPA很多朋友在选型时会纠结spring data jpa和mybatis-plus。我的体会是如果项目里复杂SQL很多、需要精细控制分页和批量更新MyBatis-Plus更顺手如果主要是标准CRUD且重度依赖对象关系映射JPA开发效率更高。一旦涉及多数据源JPA虽然也能配置多个EntityManagerFactory但配置量和坑比MyBatis-Plus更大。所以我后来认定了MyBatis-Plus这条路。1.2 整体架构dynamic-datasource、Druid、SQL过滤器各自干什么这里把整条链路拆开讲。最上层是Spring Boot应用业务代码通过DS注解指定数据源。中间层是dynamic-datasource的动态路由它相当于一个“交通指挥中心”内部有一个DataSource的Map类似{master: DruidDataSource, order: DruidDataSource}。当方法上标注DS(order)AOP切面会在方法执行前把key设为order等到真正需要连接数据库时DynamicRoutingDataSource这个AbstractRoutingDataSource子类就根据key从Map里取对应的连接池。再往下是Druid连接池。它不只是连接池还内置了一套Filter链也就是标题里说的SQL过滤器的一部分。Druid的Filter分成很多种stat负责监控统计记录每个SQL的执行次数、耗时、返回行数wall是SQL防火墙能拦截明显的注入语句和危险操作config负责配置解密常用于数据库密码的“密文配置”。这些Filter不是靠硬编码判断而是以插件形式挂在连接池上每条SQL在执行时都会经过它们。同时候还有一个容易被忽略的层面MyBatis-Plus自身的拦截器。Druid的Filter作用在JDBC层它能看到一条SQL已经拼好所有参数后的样子而MyBatis-Plus的Interceptor作用在MyBatis执行SQL之前可以拿到Mapper方法名、参数对象、BoundSql甚至是当前数据源key。也就是说如果你想做“SQL审计日志”或者“只读库禁止写操作”这样的业务规则光靠Druid的WallFilter有点杀鸡用牛刀更合适的是在MyBatis-Plus的InnerInterceptor里写自定义逻辑。这两个层面各管一段配合起来才能让“SQL过滤器”既管安全又管业务。选择Druid而不是HikariCP我的初衷主要就两个一是Druid有现成的监控页面可以直接查看慢SQL和连接池状态二是WallFilter能做SQL注入防护虽然不能替代所有安全手段但对团队来说多一层兜底总是好的。Druid性能并不差只是配置项很多初期会觉得繁琐。实际用下来监控页带来的收益远大于配置成本。多数据源情况下Druid会为每个数据源创建独立的连接池所以监控页里可以分数据源查看各自的连接数和SQL统计这对排查“某个库连接池被打满”这类问题特别有用。2. 依赖与基础配置一把梭还是踩坑版2.1 最小依赖清单先给出我验证过的pom依赖。我的环境是Spring Boot 2.7.18MyBatis-Plus 3.5.3dynamic-datasource 3.6.1Druid 1.2.20。如果你用Spring Boot 3.x需要把mybatis-plus-boot-starter换成mybatis-plus-spring-boot3-starter其它基本不变。dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.6.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency这里有一个容易产生的疑问为什么要引入druid-spring-boot-starter而不是只引入druid核心包因为starter会把DruidDataSourceAutoConfigure注册进来同时支持spring.datasource.druid.*配置自动绑定。如果你用的是dynamic-datasource它本身允许在spring.datasource.dynamic.druid.*下配置通用Druid参数不需要自己写DruidConfig类。但如果想让监控页和Filter生效还是要借助Druid的Spring Boot自动配置能力。这一块我踩过坑当时只引入了druid结果stat-view-servlet完全不生效后来换成starter才顺利跑起来。还有一个细节Spring Boot 2.x默认数据源是HikariCP在你引入Druid starter后Spring Boot仍然可能用Hikari自动创建数据源。dynamic-datasource接管后它内部创建的是DruidDataSource所以默认DataSource冲突问题不会出现。但如果项目里以前写过自定义DataSourceConfig最好删掉不然动态路由和这个自定义Bean容易互相干扰。2.2 第一步application.yml中配置多数据源和Druid这一步是整个配置的重点。我用两个数据源做演示一个是主库master一个是订单库order。在实际项目中可能是“主站库”、“日志库”或“报表库”按业务起名字就好。注意strict参数很关键先看配置spring: datasource: dynamic: primary: master strict: true druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 filters: stat,wall,config filter: stat: enabled: true db-type: mysql log-slow-sql: true slow-sql-millis: 5000 merge-sql: true wall: enabled: true config: multi-statement-allow: false none-base-statement-allow: false select-where-alway-true-check-enabled: false config: enabled: true connection-properties: config.decrypttrue;config.decrypt.key你的公钥 web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/* session-stat-enable: true stat-view-servlet: enabled: true url-pattern: /druid/* login-username: druid login-password: druid123 allow: 127.0.0.1 deny: reset-enable: false datasource: master: url: jdbc:mysql://localhost:3306/main_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver order: url: jdbc:mysql://localhost:3306/order_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: assign_id先解释primary和strict。primary表示默认数据源当业务代码没有加DS注解时所有SQL都会走这个库。strict我在生产环境建议设成true它的作用是当指定的数据源名字不存在时直接抛异常而不是默默退回primary。很多线上诡异问题都出在“名字写错但不报错SQL全跑到了主库”这种情况strict能尽早暴露配置错误。当然如果你的业务中有大量未指定数据源但希望走主库的场景可以把strict设为false。接着看Druid公共参数。initial-size、min-idle、max-active这些是连接池的基础参数我建议结合压测结果设置不要照抄网上的值。比如一个订单库高峰期需要70个连接你却只给20那迟早会出现wait millis 60000, active 20这种报错。validation-query设为SELECT 1配合test-while-idle可以在连接空闲时进行探活避免MySQL主动断开后拿到失效连接。test-on-borrow一般设为false因为每次拿连接都校验会影响性能连接断了靠test-while-idle兜底就行。还有一个容易被忽略的参数merge-sql它是StatFilter的配置。打开后Druid会把结构相同但参数不同的SQL合并成一条统计比如select * from user where id1和select * from user where id2合并成一条显示方便分析SQL整体性能。但如果你要精确看每个参数值对应的SQL分布可能不需要merge这个看自己需求。log-slow-sql设置为true配合slow-sql-millis可以让慢SQL直接输出到应用日志比只看监控页方便得多。2.3 Druid监控页面与SQL过滤器怎么开起来很多朋友配置完Druid没效果第一反应是代码写错了其实监控页和Filter完全是配置驱动的。上面的yaml里已经包含两个关键模块web-stat-filter和stat-view-servlet。web-stat-filter负责拦截所有Web请求采集URL、Session、SQL执行数据stat-view-servlet负责提供监控页面入口。配置完成后重启应用浏览器访问http://localhost:8080/druid/index.html输入druid和druid123就能看到监控面板。如果项目配置了server.servlet.context-path访问路径也要带上比如http://localhost:8080/myapp/druid/index.html。另外如果项目里有Shiro或者Spring Security这类权限框架一定要把/druid/**加入匿名访问白名单或者配置成允许登录用户访问否则监控页会被拦截器挡在外面。我在一个老项目里就遇到过配置全对但一直404排查半天才发现是Shiro过滤器把请求交给了登录页。关于exclusions务必把静态资源和/druid/*本身排除掉。如果不排除/druid/*监控页的CSS和JS会请求/druid/*.js结果也被WebStatFilter拦截页面样式全丢。这个问题非常经典可以先记下来。2.4 mapper-locations与XML同目录问题再补充一个和MyBatis-Plus多数据源常搭配的小问题。很多新项目习惯把Mapper接口和XML文件放在同一个包下比如com/example/dao但又发现Mapper.xml没有生效。MyBatis-Plus默认只扫描classpath*:/mapper/**/*.xml如果你的XML不在这个路径下自然找不到。如果想实现“XML和Mapper同包”需要把mapper-locations改成对应的包路径比如mybatis-plus: mapper-locations: classpath*:com/example/dao/**/*.xml同时因为Maven默认只把src/main/java下的.java文件打进classpath不会把src/main/java下的.xml复制到target目录所以你还要在pom.xml的build里配置资源过滤build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources /build这个做法在单体模块里很实用省去了单独建resources/mapper目录的麻烦。不过在微服务多模块项目里我更推荐把XML统一放在resources/mapper下因为模块打包和依赖隔离时classpath*:mapper/**/*.xml这种写法更稳不容易出现资源覆盖问题。3. 数据源切换与事务控制的实操细节3.1 DS注解的基本用法配置搞定后最核心的用法就是DS注解。它可以放在Service实现类上、方法上、Mapper接口上也可以放在Mapper方法上。优先级是方法上的注解大于类上的注解类上的注解大于接口上的注解。我实际项目里的习惯是在Service层方法上加DS这样整个方法内的所有Mapper操作都会走同一个数据源逻辑最清晰。下面是一个很典型的用法Service RequiredArgsConstructor public class OrderServiceImpl extends ServiceImplOrderMapper, Order { private final OrderMapper orderMapper; DS(order) public ListOrder listOrders() { return orderMapper.selectList(new LambdaQueryWrapperOrder() .eq(Order::getStatus, 1) .orderByDesc(Order::getCreateTime)); } DS(master) public void updateOrder(Order order) { updateById(order); } }这里listOrders()查询的是订单库updateOrder()更新主库。MyBatis-Plus的LambdaQueryWrapper写法没有受多数据源影响非常丝滑。但是有一个重要的坑DS底层使用Spring AOP实现如果你在同一个类里调用被DS标注的方法比如this.listOrders()代理是不会生效的数据源切换会失败。Spring AOP代理只拦截外部Bean调用不拦截内部调用这是所有注解式切面的共通问题。解决办法也很简单把需要切数据源的方法放到另一个Service类里或者注入自身代理Service public class OrderServiceImpl { Autowired private OrderServiceImpl self; public void process() { // 使用 self 调用才会走代理 self.listOrders(); } }3.2 事务与DS的顺序问题这是多数据源项目里翻车率最高的问题。先看这个错误写法Transactional DS(order) public void createOrder(Order order) { orderMapper.insert(order); otherMapper.updateSomething(); }咋一看没问题但Spring事务拦截器和数据源切面是有执行顺序的。Transactional的事务拦截器优先级要高于DS切面也就是说事务先开启连接先绑定到线程上然后才去切换数据源。这样一来事务里用的连接可能还是事务开启前那个默认数据源的连接DS(order)根本没有真正生效。结果是订单数据写进了主库或者在切换后发了异常。解决方法有两个。第一个使用dynamic-datasource提供的DSTransactional这个注解内部对事务和动态数据源做了处理可以保证数据源切换在事务开启前完成DSTransactional DS(order) public void createOrder(Order order) { orderMapper.insert(order); }第二个把DS和Transactional分层隔离。例如将只读查询方法放在一个Service中事务写在另一个Service中Service public class OrderWriteService { Transactional public void createOrder(Order order) { orderReadService.insertIntoOrderDb(order); } } Service public class OrderReadService { DS(order) public void insertIntoOrderDb(Order order) { orderMapper.insert(order); } }这里要特别说明DSTransactional只支持本地事务不支持跨数据源的分布式事务。如果一个业务需要同时写主库和订单库并且要求要么都成功要么都失败单靠数据库本地事务是办不到的需要引入Seata、ShardingSphere或者手写补偿方案。很多刚接触多数据源的人以为加了DSTransactional就能解决“分布式事务”这是不对的。3.3 异步线程/定时任务中的数据源丢失另一个容易踩的坑是异步线程丢数据源。DS把数据源key存在ThreadLocal里而Async注解会开启新线程新线程的ThreadLocal是空的。如果你写了一个异步方法去查订单库直接调用带DS(order)的Mapper很可能查回主库或者报找不到默认数据源。我的经验是异步方法内部不要依赖调用方的DS而是自己再指定一次数据源。Spring的代理会拦截异步方法上的注解所以只要异步方法本身标注了DS新线程在执行时就会重新设置key。例如Async DS(order) public CompletableFutureListOrder asyncQueryOrders() { return CompletableFuture.completedFuture(orderMapper.selectList(null)); }如果是使用线程池执行Runnable且Runnable内部需要操作某个指定库直接在Runnable的具体类或方法上加DS同样有效。不要梦想ThreadLocal能自动跨线程传递这不是它的设计目标。3.4 分页插件与多数据源兼容MyBatis-Plus的分页插件很常用平时单库没什么好说的但多数据源下有一个隐藏问题PaginationInnerInterceptor构造时一般要指定DbType例如new PaginationInnerInterceptor(DbType.MYSQL)。如果所有数据源都是MySQL那没问题万一有一个库是PostgreSQL或者Oracle分页SQL方言就会不对导致分页变成全表查询再内存分页甚至直接报语法错误。目前dynamic-datasourceMyBatis-Plus没有自动根据当前数据源切分页方言的现成机制所以我对团队的要求是多数据源尽量保证同一数据库类型避免混搭。实在需要混搭就把不同数据库类型的数据源拆到不同SqlSessionFactory各自注册对应的分页插件。这个方案虽然配置复杂但至少不会在线上出现诡异的分页问题。4. SQL过滤器的三大玩法监控统计、防注入、SQL改写与审计4.1 StatFilter慢SQL和统计报表怎么查前面提到filters: stat开启后Druid会在监控页生成SQL监控列表。进入/druid/index.html点击菜单栏的“SQL监控”能看到每条SQL的执行次数、总耗时、最慢耗时、返回行数等。这些数据对性能分析非常有用。比如一条SQL执行次数很高但平均耗时很小可以判断是否命中了索引一条SQL总耗时占了整个池子的30%基本能锁定为第一优化对象。StatFilter还有一个很实用的配置慢SQL日志。我把log-slow-sql设为trueslow-sql-millis设为5000然后配合logback的日志异步输出慢SQL会单独打到日志文件里。线上排查问题时直接搜索slow sql关键字能快速找到性能瓶颈不需要登上监控页。这个做法在没时间看监控页的深夜告警情况下简直是救命稻草。要注意merge-sql开启后由于相似SQL被合并你看到的“SQL文本”可能少了具体参数。如果你想精确定位到某一条SQL是哪个业务方法发出来的建议在StatFilter之外再在MyBatis-Plus层做一层SQL日志办法很简单在yaml里配置mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台会打印每个Mapper方法的预编译SQL和参数虽然生产环境不建议常年打开但排查问题时临时开一会儿性价比极高。4.2 WallFilterSQL防火墙的规则与误杀处理WallFilter是Druid最有特点的功能也是“SQL过滤器”最硬核的部分。它会把SQL文本做词法解析、语法分析然后按危险规则去匹配。比如检测到where 11这种恒真条件可能意味着注入尝试检测到SQL里带了/*注释可能是在尝试绕过过滤检测到同时执行多条SQL语句也会被拦下来。这些规则默认开启不需要额外写代码。但实际业务里WallFilter很容易“误伤”正常SQL。我举个例子MyBatis-Plus的LambdaQueryWrapper拼SQL时默认不会生成where 11但有些老系统里手写的SQL会带where 11来拼接动态条件。这种SQL在WallFilter的select-where-alway-true-check-enabled规则下很可能被直接拦截。如果你的系统里大量使用这种写法就需要把这个规则关掉或者引导开发规范改造成where标签。另一个常见误杀是注释SQL。比如在XML里写了一段带注释的SQL特别是一些老SQL会用/* 这是备注 */来解释业务逻辑。WallFilter有commentAllow配置默认情况下有可能会拦截带注释的SQL。如果确认业务SQL都是自己人写的可以放宽spring: datasource: dynamic: druid: wall: enabled: true config: comment-allow: true multi-statement-allow: false none-base-statement-allow: false update-where-none-check-enabled: trueupdate-where-none-check-enabled这条我要重点说。它的作用是强制UPDATE语句必须带WHERE条件防止一条更新语句把整张表数据全改掉。虽然这会让一些“批量全表更新”场景不便但线上数据安全永远高于方便。团队里有毛手毛脚的新人或者有从Navicat里直接拖出来跑的SQL片段这条规则值得打开。multi-statement-allow则要谨慎。有些报表系统会在一条连接里执行多段SQL比如用分号分隔的多个INSERT默认WallFilter会拦截。如果你想放行就要设multi-statement-allow: true但同时相当于对注入攻击敞开了一条大门。因为很多注入攻击就是利用多语句执行来扩大危害的。我的建议是除非业务有明确需求否则保持false并在代码里改成逐条执行。4.3 MyBatis-Plus层的InnerInterceptorSQL审计、改写和读写分离兜底Druid的Filter管的是底层的SQL执行但对“当前查询来自哪个数据源”这个信息它是感知不到的。而MyBatis-Plus的InnerInterceptor可以它处于MyBatis执行链路中能拿到当前Mapper方法、参数和BoundSql。所以很多“SQL审计”、“数据权限过滤”、“读写分离兜底”都在这一层实现。先看最常见的SQL审计拦截器。你可以自定义一个InnerInterceptor在beforeQuery和beforeUpdate里记录SQL、耗时、数据源keypublic class SqlAuditInterceptor implements InnerInterceptor { Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { String methodId ms.getId(); String sql boundSql.getSql(); // 记录被执行的Mapper方法和SQL System.out.println([SqlAudit] method methodId , sql sql.replaceAll(\\s, )); } }然后在配置类里加载Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new SqlAuditInterceptor()); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这里有一点要注意拦截器的执行顺序是加入顺序。如果你的自定义拦截器会修改SQL它应该放在PaginationInnerInterceptor之前否则分页插件先处理好SQL你再改动可能导致分页失效。如果只是记录日志放前面放后面影响不大但为了通用性我建议自定义拦截器统一放第一个。还有一个很实用的场景只读从库兜底。假设公司规定slave库只允许SELECT但有同事偷懒在Service里直接调用DS(slave)去INSERT。WallFilter能帮你防住一部分但它不知道业务规则。在MyBatis-Plus拦截器里可以这样判断Override public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) { String methodId ms.getId(); if (methodId.startsWith(com.example.mapper.SlaveMapper)) { throw new RuntimeException(slave库禁止写操作); } }当然更优雅的做法是结合DynamicContextHolder拿到当前数据源key然后对所有非master数据源统一禁止写操作。这个思路可以灵活扩展比纯靠Druid WallFilter更贴合业务。5. 数据库密码加密Druid、多数据源和若依风格5.1 加密原理与ConfigTools“若依框架集成druid实现数据库密码加密功能”这个热词最近讨论度很高。其实若依用的就是Druid自带的ConfigTools并不是什么黑科技。它的原理是RSA非对称加密生成一对公私钥用私钥对数据库密码加密得到密文配置文件中存放密文和公钥Druid的ConfigFilter在创建连接池时读取公钥对密文进行解密得到真正的明文密码。核心价值在于配置文件泄露后别人看到的是不可逆的密文和一把公钥而不是明文的数据库密码。虽然这个过程不能说绝对安全因为拿到公钥和密文后理论上可以暴力穷举弱密码但至少能挡住大部分误提交代码仓库、日志打印导致的泄露问题。这是“配置加一层密码保险”的做法比放在Nacos明文里强太多。5.2 使用步骤第一步使用Druid的ConfigTools生成密文。需要先确认druid jar包路径然后在命令行执行java -cp druid-1.2.20.jar com.alibaba.druid.filter.config.ConfigTools 你的数据库密码执行后会输出三行内容privateKey、publicKey、password。示例privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASC... publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJl... password: VVFSfIE4U5hIzPb6M0u0f3iP6eK...第二步修改application.yml。把spring.datasource.dynamic.datasource.master.password替换成生成的password然后在公共druid配置里开启解密并写入公钥spring: datasource: dynamic: druid: filters: stat,wall,config filter: config: enabled: true connection-properties: config.decrypttrue;config.decrypt.keyMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJl... datasource: master: url: jdbc:mysql://localhost:3306/main_db?... username: root password: VVFSfIE4U5hIzPb6M0u0f3iP6eK...这里要特别强调两个坑。第一filter.config.enabled: true是必须的很多人只配置了connection-properties里的config.decrypttrue和config.decrypt.key但Filter没有启用Druid根本不会执行解密启动时会报BadPaddingException或者提示“decrypt password error”。第二filters: stat,wall,config里的config也不能漏。这个filters是老的写法如果同时写了filter.config.enabled实际会以Filter配置类为准但我建议两者都保持一致避免后续升级配置时行为不一致。第三步验证启动日志。正常启动后Druid会提示配置成功连接池正常初始化。你可以在监控页的数据源页面看到数据源配置但密码不回显所以很好验证。如果有多个数据源可以每个库独立生成密文各自配置。比如order库的密码可能和master不同那就再执行一次ConfigTools然后替换对应数据源的password。公钥也可以不同但我们通常会在公共druid配置里写同一个公钥然后所有数据源都用它加密。私钥只用于生成密文生成完成后就应该妥善保管在密码管理器里绝不要提交到Git仓库。5.3 配置安全建议最后补充三条实操建议。第一数据库连接地址、用户名、密码不要硬编码在代码里尤其不要写在常量类中。第二密文虽然不怕人看但一旦私钥泄露所有密文都可以被还原所以私钥的保管等同于数据库密码的保管最好是放进公司的密钥管理系统。第三加密只防配置泄露不防程序内部被拖库。如果你的应用服务器被入侵攻击者拿到JVM内存一样能翻到解密后的连接信息。所以密码加密和防火墙、权限管控是并列关系不能互相替代。6. 常见问题与排查技巧实录6.1 故障速查表这里把我在生产环境遇到过的典型问题整理成表格方便直接参考现象常见原因解决办法多数据源切换没生效一直走主库DS标注在同类内部调用方法上Spring代理没拦到拆到另一个Bean或使用注入自身代理调用加了Transactional后DS失效事务拦截器先开启并绑定了连接改用DSTransactional或把数据源切换放到独立Service中Druid监控页404stat-view-servlet没配置或路径不对被权限框架拦截检查url-pattern和context-path放行/druid/**监控页有数据但SQL统计空白web-stat-filter没有开启或exclusions里排除了SQL统计路径确认web-stat-filter.enabledtrueurl-pattern/*WallFilter误杀正常SQLwhere 11、注释、多语句等规则触发按业务放开select-where-alway-true-check-enabled或comment-allow等配置分页SQL方言不对多数据源跨越不同数据库类型DbType固定了统一数据库类型或拆分多套SqlSessionFactory数据库密码解密失败没启用filter.config.enabled或connection-properties键名错误确认filters含configconfig.decrypt.key是公钥重新生成密文6.2 排查实操经验先说启动日志。dynamic-datasource启动成功后会打印类似dynamic-datasource initial load [master, order]的日志如果你没看到预期数据源说明配置没被解析先检查拼写和缩进。YAML的缩进错位是新手最容易踩的坑尤其datasource嵌套层级多的时候看着没问题但Spring读取的其实是null。其次是定位数据源切换是否生效。可以在AOP方向验证在Service方法入口打印当前数据源key使用DynamicContextHolder.peek()可以拿到当前线程的数据源名。比如DS(order) public void test() { System.out.println(当前数据源: DynamicContextHolder.peek()); }如果输出的是order说明切换成功如果输出null或者还是master那就要排查代理链路和事务顺序了。我在定位一个诡异问题时就是靠这行输出发现Transactional先于DS执行导致切库失效。另一个真实案例。去年上线了一个定时任务集群每天晚上跑批把报表库的SQL全打到主库导致主库CPU告警。排查后发现定时任务方法上加了Transactional而DS(report)写在了同一个方法上。按照我们上面的分析事务先开数据源切换没成功所有SQL全部走默认主库。把DS和Transactional拆层后慢SQL立刻分散到了报表库主库压力降了下来。这个案例让我从此立了一条规矩多数据源方法上尽量不叠加事务注解除非用DSTransactional。6.3 分数据源控制Filter规则最后聊一个比较少有人提到的经验dynamic-datasource支持在每个数据源内部单独覆盖druid配置这就意味着你不必对全局所有数据源都执行同一套WallFilter规则。比如主库需要最严格的SQL防火墙报表库则因为复杂分析SQL太多适当放宽。可以在数据源层级单独写spring: datasource: dynamic: datasource: master: url: jdbc:mysql://localhost:3306/main_db username: root password: ... druid: filter: wall: enabled: true config: comment-allow: false report: url: jdbc:mysql://localhost:3306/report_db username: root password: ... druid: filter: wall: enabled: false这个能力很关键。我一开始把所有数据源都按同一套严格wall规则跑结果报表组同学反馈大量SQL执行失败查下来是WallFilter拦了几十种复杂查询。后来给report数据源单独关掉wall仅保留stat监控主库继续开启严格规则两边都顺畅了。这说明技术方案一定要结合业务场景配置不能一把梭。最后说句实在话多数据源、Druid、SQL过滤器这些组件组合在一起确实能解决很多实际问题但它们不是越多越好。我在实际项目中吃过亏一开始把wall、stat、config、log4j全部打开结果报表库复杂SQL被拦截线上告警不断。后来学会分数据源控制Filter规则才真正把监控和防注入用起来。如果你正在做类似改造建议先照着这篇的配置跑通最小环境确认数据源切换正常再逐步打开防火墙和监控别一上来就全量上线给自己留足排查空间。