单端口 · 双协议 · 字节级 wire 缓存 · Rust + Tokio

在数据库前面,加一层
能听懂 SQL 的缓存

MySQL/PostgreSQL Cache Proxy 是一个高性能 Rust 代理: 在客户端和真实数据库之间完成鉴权 · 连接复用 · 字节级响应缓存 · 表级失效, 两种协议共享同一份基础设施,业务侧零改造。

MySQL 5.7+ PostgreSQL 12+ 同一端口同时接
2
协议 · 单端口同时承接
~1ms
缓存命中端到端 (vs ~40ms)
100%
wire 字节回放 · 任何列类型
0
业务侧改造成本
特性

一个代理,两种协议,零妥协

所有性能、安全、一致性细节都被设计为「不需要业务侧改一行代码」。MySQL 客户端走 MySQL 路径,PostgreSQL 客户端走 PG 路径,两者共享同一份连接池、缓存、失效语义。

单端口双协议(启动期自动识别后端)

同一个 listen 既可对外说 MySQL 也可对外说 PostgreSQL;启动时代理拨测后端(MySQL Initial Handshake / PG SSLRequest)自动识别要走哪条路径,也可用 backend.protocol 显式覆盖。

wire-level 字节级缓存

命中走 write_all 字节回放,不解码任何列类型;JSON / GEOMETRY / DECIMAL / DATETIME 全部字节级一致,无 round-trip 损耗。

SQL 表级智能失效

sqlparser 解析每条 SQL 抽出受影响表集合;INSERT/UPDATE/DELETE 落在表 T,则所有命中过 T 的缓存条目(含所有参数变体)原子失效。

(user, sha256(pw), db) 池化

按凭据隔离的空闲连接池,跨客户端会话复用已认证的后端连接。release 时跑 DISCARD ALL(PG)或 COM_RESET_CONNECTION(MySQL)清场。

异步文件日志

结构化的每 SQL / 缓存事件日志由单独 writer 线程批量落盘,调用线程只入队;磁盘 I/O 不会阻塞查询热路径。

TCP_NODELAY 双向开启

消除 ~40ms 的 Nagle + delayed-ACK 死等。这一个开关是把端到端从 40–80ms 拉回 1–2ms 的关键,不是噱头。

集群级失效广播(可选)

多副本部署时,本地 DML 引发的 invalidate_table 通过共享 broadcast-host WebSocket 中转扇出到同 group 的所有副本,对端就地清掉对应缓存项;中转不会回送给发送方,单节点保持关闭即可。

连通性探针 → CACHE OFF(可选)

维持一条出站 wss:// 长连接。链路断开 + 重连连续 N 次失败 → 强制所有 SQL 绕过缓存直通后端(CACHE OFF),握手再次成功后自动恢复(CACHE ON)。

架构

一张图看完查询流向

客户端 ⇄ 代理 ⇄ 后端三段,每段都按各自协议走原生 wire;代理在中间做缓存判定、表级失效与连接池路由。

MySQL / PG
client
Cache Proxy
auth · cache · pool
MySQL / PG
backend
协议识别
startup probe
客户端鉴权
cleartext → backend
缓存查询
(sql, tables, bind)
后端 (miss)
pooled wire client
字节缓存
raw wire bytes
多副本

多个代理副本之间的失效互通

同一个后端 DB 前部署多个代理副本时,让所有副本通过 broadcast-host 共享一份"哪些表刚被改"的实时视图。中转按 group 扇出,写副本的 invalidate_table 会把读副本的对应缓存项也清掉。详见 docs/broadcast-invalidation.md

proxy A
UPDATE users …
broadcast-host
group fan-out
proxy B / C
invalidate_table("users")
查询生命周期

从入连接到回字节,6 步走完

无论 MySQL 还是 PG,每条 SQL 都要走完这 6 步;缓存命中只走前 3 步,命中即回字节。

1
接连
TCP_NODELAY
协议在启动时已拨测确定
2
鉴权
cleartext 取得密码
sha256 算池 key
3
解析
sqlparser 抽
受影响表集合
4
查缓存
命中:write_all
回字节
5
查后端
miss:池中拿连接
捕获原始字节
6
缓存/失效
SELECT 写入
DML 表级失效
协议对照

双协议路径,等价语义

两条路径在缓存、连接池、鉴权三处保持完全等价的行为;差异仅在各自 wire 协议的细节里。

MySQL 路径
客户端鉴权插件mysql_clear_password
客户端默认插件兜底AuthSwitchRequest
后端鉴权方式native_password / caching_sha2 (fast)
缓存命中包COM_QUERY 全响应字节
包重写sequence_id 重新打包
缓存条件单语句 · 无 MORE_RESULTS · 无 ERR
连接 release 命令COM_RESET_CONNECTION
实现自实现 wire 客户端 · 无 mysql_async
PostgreSQL 路径
客户端鉴权方式AuthenticationCleartextPassword
支持模式simple query · extended query
后端鉴权方式SCRAM-SHA-256 / MD5 / cleartext
缓存命中包 (simple)RowDescription + DataRow + CmdComplete
缓存命中包 (extended)同上 + 重发 ReadyForQuery
缓存条件单语句 SELECT · ReadyForQuery 'I'
连接 release 命令DISCARD ALL
实现自实现 wire 客户端 · zero-copy 批量转发
性能

每个数字都来自真实压测

详细排查与修复过程见 docs/performance-tuning.md,最重要的几个点列在这里。

端到端时延
直连后端~1–2 ms / query
未优化的代理40–80 ms / query
开 TCP_NODELAY 后~1–2 ms / query
缓存命中< 1 ms / query
命中 vs 后端~10–40× 快
热路径优化
TCP Nagle 移除双向 setnodelay
PG 扩展查询缓冲单 Vec · 0 alloc
命中 CPU仅一次 write_all
日志异步 writer 线程
认证开销池化后摊销为 0
最小化配置

config.yaml 一份就够

凭据不进配置 — 代理用客户端发来的 user/password/database 直接打上游。

proxy:
  listen: "0.0.0.0:3307"

backend:
  host: "127.0.0.1"
  port: 3306
  protocol: "mysql"          # or "postgresql"

  # 共享池:MySQL / PG 路径都消费这份配置
  pool:
    enabled: true
    max_per_key: 16
    idle_ttl_secs: 300
    reset_query: "DISCARD ALL"   # MySQL 自动映射为 COM_RESET_CONNECTION

cache:
  enabled: true
  ttl_secs: 300
  max_entries: 10000

# 可选:出站 WSS 长连接做心跳,断开 + 重连失败 N 次 → CACHE OFF
connectivity_probe:
  enabled: false
  url: "wss://myip.qq.com"
  interval_secs: 5
  reconnect_failures_for_cache_off: 3

# 可选:多副本部署时通过 broadcast-host 互通失效
broadcast:
  enabled: false
  url: "ws://gy.duguying.net:9940"
  group: "default"
  ping_interval_secs: 15

log:
  level: "info"
  stdout_level: "warn"
  path: "log/database-cache-proxy.log"
  file_async: true

5 分钟跑通你的第一条 [HIT]

跟着「快速开始」一步步来,半小时内即可看到第一条 cache 命中日志。