单端口 · 双协议 · 字节级 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 的关键,不是噱头。

集群模式(可选)

一条到 broadcast-host 的 WebSocket 长连接同时承担两件事:(1) 失效广播——多副本之间互通本地 DML 触发的 invalidate_table;(2) 连通性兜底——重连连续 N 次失败 → 强制 CACHE OFF(直通后端),恢复时先清空本地 cache 再翻回 CACHE ON,避免 OFF 期间外部写入让旧快照被读到。单节点保持关闭即可。

架构

一张图看完查询流向

客户端 ⇄ 代理 ⇄ 后端三段,每段都按各自协议走原生 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 前部署多个 database-cache-proxy 副本时,每个副本各自维护独立的内存缓存。一个副本上的 INSERT/UPDATE/DELETE 会通过 broadcast-host 中转扇出,让其它同 group 的副本就地清掉对应的缓存项——避免读副本拿到脏数据。

业务客户端 (mixed clients) MySQL · PG · driver / CLI / app L4 LB(可选) haproxy / nginx-stream / k8s svc proxy A :3307 · cache (local) UPDATE users SET ... → invalidate_table("users") proxy B :3307 · cache (local) SELECT ... FROM users 命中 / 失效后回源 proxy C :3307 · cache (local) SELECT ... FROM users 命中 / 失效后回源 broadcast-host :9940 · WebSocket fan-out group = "prod-cluster-1" ws JSON · invalidate_table 扇出到同 group 副本 后端 DB (所有副本共享) MySQL 5.7+ / PostgreSQL 12+
SQL 流(client ↔ proxy ↔ DB),按各自协议透明转发
失效广播流(proxy ↔ broadcast-host),ws JSON Text,QPS = 集群 DML 总数

每副本独立缓存

不需要外部 KV / Redis,cache 在 proxy 进程内的 DashMap;命中走字节回放,零序列化开销。

表级失效广播

本地 DML 触发 invalidate_table(t) 后异步 publish;不阻塞 SQL 路径,同 group 节点秒级同步。

broadcast-host 单点容忍

中转挂了 SQL 流量不受影响,自动重连;期间退化为"每副本独立 cache",恢复后立刻回到集群一致状态。

水平扩展友好

加副本 = 起一个新的 proxy 容器、配同样的 group;自动加入失效广播网络,无需配置中心。

查询生命周期

从入连接到回字节,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

# 可选:集群模式(一条 ws 连接同时做失效广播 + 连通性判定)
# 重连连续失败 → CACHE OFF;恢复 → 清缓存 → CACHE ON
cluster:
  enabled: false
  url: "ws://gy.duguying.net:9940"
  group: "default"
  ping_interval_secs: 15
  reconnect_delay_secs: 5
  reconnect_failures_for_cache_off: 3

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

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

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