2010 字
约 6 分钟
21
S06-X03 · P6Spy:打印完整 SQL 与耗时,以及生产为什么必须关

MyBatis 打印的 SQL 长这样:select * from article where id = ?,下一行才是 Parameters: 42(Long)。两行拼起来才是真正发到数据库的语句——参数一多、类型一复杂,靠肉眼拼就是灾难;而慢查询到底慢在解析、执行还是网络,MyBatis 的日志一个字都不提。

P6Spy 补的正是这个缺口:它在 JDBC 驱动外面包一层,拦住 PreparedStatement 的最终执行,把参数已经填好的 SQL、耗时、连接信息一起吐出来。


1. 它站在哪:不是日志框架,是驱动代理

层 组件 看到的东西
ORM MyBatis / JPA 带 ? 的模板 SQL + 参数数组
连接池 HikariCP Connection 对象
P6Spy P6SpyDriver 真实下发的 SQL、执行毫秒数、连接 id
真驱动 com.mysql.cj.jdbc.Driver 实际网络协议

机制很朴素:把 driver-class-name 换成 com.p6spy.engine.spy.P6SpyDriver,由它加载真驱动,再把 URL 的 jdbc: 前缀改写成 jdbc:p6spy:。连接池拿到的还是标准 Connection,业务代码一行不用改。

2. 接入三步

第一步,加依赖:

// <dependency>
//   <groupId>com.p6spy</groupId>
//   <artifactId>p6spy</artifactId>
//   <version>3.9.1</version>
// </dependency>

第二步,改驱动与 URL:

spring:
  datasource:
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://${DB_HOST:127.0.0.1}:3306/clarity?useSSL=false&serverTimezone=Asia/Shanghai
    username: ${DB_USER:clarity}
    password: ${DB_PASS:}

第三步,把 spy.properties 放到 src/main/resources 根目录。driver-class-name 必须显式写,不能靠 Spring Boot 从 URL 推断——jdbc:p6spy: 不是任何真驱动的合法前缀,自动推断会抛 Failed to determine a suitable driver class,这是接入 P6Spy 最高频的一次失败。

3. spy.properties 关键项

键 作用 建议值
driverlist 真驱动类名 com.mysql.cj.jdbc.Driver
appender 输出目的地 dev 用 StdoutAppender,排障用 FileLogger
logfile 文件名 spy.log,注意它落在进程工作目录
append 追加还是覆盖 true,否则每次启动清空
logMessageFormat 格式器 CustomLineFormat 才能自定义
customLogMessageFormat 自定义模板 见下
excludecategories 不打印的类别 info,debug,result,commit,resultset
deregisterdrivers 关闭时注销驱动 true,避免重复注册告警
useprefix 是否保留 p6spy 前缀 false 时输出更干净
driverlist=com.mysql.cj.jdbc.Driver
appender=com.p6spy.engine.spy.appender.StdoutAppender
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=%(currentTime) | %(executionTime)ms | %(category) | conn %(connectionId) | %(sqlSingleLine)
excludecategories=info,debug,result,commit,resultset
deregisterdrivers=true

%(sqlSingleLine) 把换行压成空格,一行一条便于 grep;想看原始缩进用 %(sql)。

输出量还能再收一层:executionThreshold 只打印超过阈值的语句,filter 用正则整条跳过。这比在日志框架里写过滤器精确,因为它在拼串之前就把语句放掉了:

# 只记录超过 500ms 的语句,配合 logfile 当轻量慢日志用
executionThreshold=500
# 整条排除健康检查与心跳表
filter=true
exclude=.*from health_check.*,.*from qrtz_locks.*

executionThreshold 的单位是毫秒。它适合临时排查某个接口的慢 SQL,不适合长期开着——阈值只看耗时,看不出参数是否敏感。

4. 典型输出长什么样

开启后单条查询的形态是这样(此为典型输出,实际毫秒数随环境变化):

2026-11-09 12:00:01.234 | 12ms | statement | conn 3 | select id, title, created_at from article where status = 1 and tag = 'java' order by created_at desc limit 20
2026-11-09 12:00:01.301 | 3ms | commit | conn 3 |

注意三点:参数已被替换成字面量,日期是带引号的字符串,字符串里的单引号被转义。这条日志可以直接粘进 MySQL 客户端复现,这是它相对 MyBatis 日志的唯一硬优势。

5. 为什么生产必须关

成本 具体来源 后果
CPU 每次执行都要抓调用栈定位真实调用方、拼完整 SQL 字符串 与 QPS 线性相关,高并发下可见
内存 参数展开要复制大字段 一次查 10 万字符的正文,日志里就是 10 万字符
磁盘 FileLogger 默认同步 append 单条 1KB × 日 20 万条 ≈ 200MB/天
泄漏 参数是明文,含密码、手机号、身份证、Token 日志一旦外流就是事故
干扰 混进应用日志,冲掉 traceId 关联 排障反而更慢

磁盘这一项在容器里尤其致命:spy.log 写在工作目录,很多镜像的工作目录就是挂载卷,日志爆盘会连带把应用写崩。这与"日志轮转"是两件事,P6Spy 自己不做轮转。

6. 只在 dev 生效

用 profile 隔离,让 URL 前缀跟着环境走:

# application-dev.yaml
spring:
  datasource:
    driver-class-name: com.p6spy.engine.spy.P6SpyDriver
    url: jdbc:p6spy:mysql://127.0.0.1:3306/clarity?useSSL=false

---
# application-prod.yaml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${DB_HOST}:3306/clarity?useSSL=false&serverTimezone=Asia/Shanghai

再加一道启动自检,防止有人把 dev 配置带进生产镜像:

@Component
public class DriverGuard implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        String driver = System.getProperty("spring.datasource.driver-class-name", "");
        String active = System.getProperty("spring.profiles.active", "");
        if (active.contains("prod") && driver.contains("p6spy")) {
            throw new IllegalStateException("p6spy driver detected in prod profile");
        }
    }
}

更隐蔽的形态是把 driver 写在 DB_DRIVER 环境变量里,生产环境忘了覆盖。自检比代码评审可靠。

如果不想维护 spy.properties,可以用 com.github.gavlyukovskiy:p6spy-spring-boot-starter,它把配置收进 decorator.datasource.p6spy.*,并自动处理驱动替换。代价是多一层封装,出问题时排查路径变长。

7. 与 MyBatis 日志怎么选

维度 P6Spy MyBatis log-impl
输出内容 最终 SQL(参数已替换)+ 耗时 模板 SQL + 参数数组,两行
覆盖范围 所有走 JDBC 的语句,含 JPA、手写 JDBC 只管 MyBatis 的 Mapper
耗时可信度 单条 execute 的墙钟时间 无
直接可复现 是,可粘进客户端 否,要手工拼
开销 较高(拼串 + 抓栈) 低
生产可用 不应开启 可开 debug 级别并限包名
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  level:
    com.example.clarity.mapper: debug

选择逻辑很简单:开发期要复现某条 SQL → P6Spy;长期开着的可观测性 → MyBatis debug 日志 + 慢查询日志。两者同时开只会得到重复且互相打断的输出。

8. 排错

现象 原因 处理
启动报 Failed to determine a suitable driver class 只改了 URL 没改 driver-class-name 两个一起改
没有任何输出 spy.properties 不在 classpath 根,或 appender 指向了不存在的类 确认文件位置,检查 target/classes
找不到 spy.log 写在了进程工作目录 用绝对路径配 logfile
报 No suitable driver driverlist 里的类名拼错或驱动未引入 与 pom 里的驱动核对
输出里 SQL 仍是 ? 拦截到了 Statement 而非 PreparedStatement 的最终执行 确认 excludecategories 没把 statement 排除
连接池报连接失效 版本与 Hikari 的 isValid 探测不兼容 升级到 3.9.x 并开 deregisterdrivers

9. 边界

场景 更合适的工具 理由
生产慢查询定位 MySQL 慢查询日志 / performance_schema 零应用侧开销,含执行计划
只要执行耗时分布 Micrometer + DataSource 指标 可聚合、可告警
需要 SQL 审计留痕 数据库审计插件 应用日志可被绕过
MyBatis 参数绑定问题 log-impl + Parameters: 行 不需要驱动级代理
分库分表环境 ShardingSphere 的 SQL 日志 代理层看不到逻辑 SQL 全貌

10. 自测

  1. 为什么换了 driver-class-name 之后,Spring Boot 仍然报"找不到合适驱动"?
  2. spy.log 默认写在哪,为什么容器里容易爆盘?
  3. excludecategories 默认排除哪几类,排除 statement 会怎样?
  4. %(sql) 和 %(sqlSingleLine) 的差别在什么场景下重要?
  5. 生产环境判断"P6Spy 是否被误开",除了看配置还能看什么?
  6. 一条慢查询,P6Spy 的耗时和 MySQL 慢日志的 Query_time 为什么可能不一致?
S06-X03 · P6Spy:打印完整 SQL 与耗时,以及生产为什么必须关
https://www.clxhxhhr.top/posts/5650/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。