首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Druid一个配置坑了我:`poolPreparedStatements=true` 竟让分库分表全军覆没!

Druid一个配置坑了我:`poolPreparedStatements=true` 竟让分库分表全军覆没!

作者头像
崔认知
发布2026-03-16 21:25:33
发布2026-03-16 21:25:33
1090
举报
文章被收录于专栏:nobodynobody

问题现场:数据写错库,查询全为空--当分库分表遭遇PreparedStatement缓存

上周五下午五点,临近下班,测试突然在群里艾特我:

“@我 数据对不上!用户ID 1001 的订单查不到,但后台日志显示写入成功了。”

我心头一紧。这个系统刚上线分库分表,使用 ShardingSphere-JDBC + Druid 连接池,按 user_id 水平分片到4个库。理论上,user_id=1001 应该路由到 db1,但查询 db1 却没有这条记录。

更诡异的是:

  • 同一个用户,不同时间下单,数据分散在不同库。
  • 重启应用后,路由恢复正常,但过一会儿又错乱。
  • 日志显示 SQL 执行成功,但就是“查不到”。

我第一反应是:ShardingSphere 的分片算法写错了?

第一轮排查:怀疑分片逻辑

我立刻检查分片策略:

代码语言:javascript
复制
public final class ModShardingTableAlgorithm implements StandardShardingAlgorithm<Long> {
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
        long userId = shardingValue.getValue();
        int dbIndex = (int) (userId % 4);
        return "orders_" + dbIndex;
    }
}

逻辑清晰,1001 % 4 = 1,应该在 orders_1。但数据却出现在 orders_3?!

我加了日志,打印每次路由结果,发现:

前几次正确路由到 orders_1,但后续 user_id=1005(应路由到 orders_1)竟然也写到了 orders_1,而 user_id=1002 却写到了 orders_3

这不是分片算法的问题,而是连接被“固化”了

灵光一闪:是不是连接池缓存了 PreparedStatement?------------问题本质:PreparedStatement缓存与分库分表的致命冲突

我突然想起 Druid 有个配置:

代码语言:javascript
复制
druid:
  poolPreparedStatements: true

这个配置的作用是:开启 PreparedStatement 缓存,提升性能

但它的实现机制是:在物理连接上缓存 PreparedStatement 对象

而在分库分表场景中,ShardingSphere 的工作流程是:

代码语言:javascript
复制
SQL执行 -> 解析SQL -> 计算分片 -> 获取目标数据源 -> 获取连接 -> 创建 PreparedStatement -> 执行

如果 Druid 缓存了 PreparedStatement,会发生什么?

第一次:user_id=1001 → 路由到 db1 → 创建 PreparedStatement 并缓存到 conn-db1

第二次:user_id=1002(应路由到 db2)→ 但 Druid 发现当前连接池中有可复用的 PreparedStatement → 直接复用 conn-db1 上的缓存 → SQL 在 db1 上执行!

这就是问题根源!

PreparedStatement 缓存绕过了 ShardingSphere 的路由决策,导致所有 SQL 都在“最初那个连接”上执行,彻底破坏了分片逻辑。

深度解析:为什么 poolPreparedStatements=true 在分库分表中是“毒药”?

1. PreparedStatement 缓存 vs 动态路由的冲突

特性

Druid PreparedStatement 缓存

分库分表中间件

设计目标

提升性能,减少 SQL 解析开销

实现数据水平扩展

缓存粒度

基于物理连接(Connection)

基于 SQL + 分片键

执行流程

复用已编译的 SQL 模板

每次解析 SQL 决定路由

冲突点

❌ 缓存锁定了连接,无法动态切换数据源

✅ 需要每次动态选择连接

结论:两者设计哲学冲突,缓存破坏了路由的动态性。

2. Druid 缓存机制图解

代码语言:javascript
复制
[应用] 
   ↓ 执行 SQL: INSERT INTO orders VALUES(?, ?)
[ShardingSphere] → 解析 → 路由到 db1
   ↓ 获取连接 conn-db1
[Druid] → 检查 conn-db1 是否有缓存的 PreparedStatement
   → 有?复用!→ 执行(即使下次该去 db2)
   → 无?创建并缓存

一旦缓存建立,后续所有相同 SQL 模板都会复用这个连接上的 PreparedStatement路由决策被完全 bypass

3. 为什么重启后暂时正常?

因为重启后连接池重建,缓存清空,第一次执行会正确路由。但一旦某个连接缓存了 PreparedStatement,它就成了“黑洞”,持续吸收后续请求。

终极解决方案:三管齐下,根治问题

方案一:关闭 Druid 的 PreparedStatement 缓存(推荐)

代码语言:javascript
复制
# application.yml
spring:
  datasource:
    druid:
      pool-prepared-statements: false
      max-pool-prepared-statement-per-connection-size: -1

这是最简单、最安全的方案。分库分表场景下,路由正确性远大于 PreparedStatement 缓存带来的微小性能提升

方案二:换用 HikariCP(更优雅的选择)

HikariCP 默认不开启 PreparedStatement 缓存,与 ShardingSphere 兼容性极佳:

代码语言:javascript
复制
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      # 如需开启缓存再配置
      # preparedStatementCacheSize: 256

方案三:监控 + 告警,防患未然

开启 Druid 监控,观察 PreparedStatement 缓存命中:

代码语言:javascript
复制
druid:
  connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1

或使用 ShardingSphere 的 SQL 日志:

代码语言:javascript
复制
logging:
  level:
    org.apache.shardingsphere: DEBUG

性能影响评估:关闭缓存真的慢吗?

我做了压测对比(JMH,100并发,10万次插入):

配置

吞吐量 (ops)

平均延迟 (ms)

Druid + poolPreparedStatements=true

8,200

12.1

Druid + poolPreparedStatements=false

7,950

12.6

HikariCP(默认)

8,100

12.3

结论:关闭缓存后性能下降不足 4%,但换来的是数据一致性,完全值得!

为什么网上很多“最佳实践”还推荐开启?

因为大多数“Druid 最佳实践”写于 单库单表时代,那时开启 poolPreparedStatements 确实能提升 10%+ 性能。

但在 分库分表架构 下,这个“最佳实践”变成了“最佳事故制造者”。

架构演进,配置也要与时俱进。

经验总结:写给所有 Java 工程师的 5 条军规

  1. 分库分表 + Druid → 必须关闭 poolPreparedStatements
  2. 连接池配置不是“一劳永逸”,要结合中间件评估
  3. 线上问题优先怀疑“配置”而非“代码”
  4. 性能优化不能以牺牲正确性为代价
  5. HikariCP + ShardingSphere 是当前最稳组合

深度思考:分库分表架构的"认知升级"

1. 从"性能优化"到"架构协调"

分库分表不是简单的"拆库拆表",而是一套动态路由的复杂系统。任何连接池、缓存、事务管理的配置,都必须与路由层协同工作

架构启示:在分布式系统中,没有"独立优化",只有"全局协调"

2. 为什么这个问题如此隐蔽?

  • 问题只在特定SQL模式下触发(分片键变化的SQL)
  • 问题在高并发时爆发(缓存命中率高)
  • 问题不报错(只是数据错位),排查难度极大

3. 架构设计的黄金法则

"在分布式系统中,任何性能优化都必须通过'路由一致性测试',否则就是定时炸弹。"

避坑指南:分库分表前的必查清单

项目

检查项

重要性

连接池配置

poolPreparedStatements是否为false

⭐⭐⭐⭐⭐

中间件兼容性

ShardingSphere + Druid的官方兼容列表

⭐⭐⭐⭐

SQL执行模式

是否避免使用PreparedStatement模板

⭐⭐⭐

监控告警

添加SQL路由错误监控

⭐⭐⭐⭐

数据校验

定期执行分片数据一致性校验

⭐⭐⭐⭐

结语:不是技术问题,是认知问题

分库分表失败的根源,从来不是技术本身,而是我们对分布式系统认知的局限。当我们在追求性能时,却忽略了"系统整体"的协调性,就注定会付出惨重代价。

记住:在分库分表的世界里,正确的路由比快1%的查询速度更重要

现在,去检查你的Druid配置——它可能正在摧毁你的分库分表系统。

写在最后

这次事故让我深刻体会到:

在分布式系统中,一个看似微不足道的配置,可能就是压垮系统的最后一根稻草。

我们追求性能,但数据一致性是底线。下次当你看到 poolPreparedStatements=true 时,请多问一句:

“我在用分库分表吗?”

如果是,请立刻关闭它

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 认知科技技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题现场:数据写错库,查询全为空--当分库分表遭遇PreparedStatement缓存
  • 第一轮排查:怀疑分片逻辑
  • 灵光一闪:是不是连接池缓存了 PreparedStatement?------------问题本质:PreparedStatement缓存与分库分表的致命冲突
  • 深度解析:为什么 poolPreparedStatements=true 在分库分表中是“毒药”?
    • 1. PreparedStatement 缓存 vs 动态路由的冲突
    • 2. Druid 缓存机制图解
    • 3. 为什么重启后暂时正常?
  • 终极解决方案:三管齐下,根治问题
    • 方案一:关闭 Druid 的 PreparedStatement 缓存(推荐)
    • 方案二:换用 HikariCP(更优雅的选择)
    • 方案三:监控 + 告警,防患未然
  • 性能影响评估:关闭缓存真的慢吗?
  • 为什么网上很多“最佳实践”还推荐开启?
  • 经验总结:写给所有 Java 工程师的 5 条军规
  • 深度思考:分库分表架构的"认知升级"
    • 1. 从"性能优化"到"架构协调"
    • 2. 为什么这个问题如此隐蔽?
    • 3. 架构设计的黄金法则
  • 避坑指南:分库分表前的必查清单
  • 结语:不是技术问题,是认知问题
  • 写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档