数据库基础体系 · 第 133/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
数据库代理与网关:连接复用、读写路由、分片、审计和故障边界
数据库代理(database proxy)是位于应用与数据库之间、能够建立数据库协议连接并转发请求的中间层。数据库网关(database gateway)通常包含更宽的职责:除了连接转发,还可能负责认证、租户隔离、读写路由、分片路由、审计、限流、故障转移和协议适配。
两者在工程中经常由同一个组件承担,但概念边界不同:
- 代理关注“请求如何到达某个数据库连接”;
- 网关还关注“请求是否允许、应该去哪里、以什么身份执行、失败后能否重试”。
代理不会自动改变数据库的事务语义、锁语义和复制一致性。它只是增加了一个必须被纳入正确性分析的状态层和故障边界。
一、先建立端到端模型
一个请求通常经过以下路径:
应用线程
│
│ 逻辑连接、SQL、参数、事务操作
▼
数据库代理/网关
│
│ 认证、连接池、路由、审计、限流
▼
后端数据库连接
│
├── 主库
├── 只读副本
└── 某个分片
这里至少存在三种不同的“连接”概念:
- 应用逻辑连接:应用从客户端驱动获得的连接对象。
- 代理前端连接:代理与应用之间的 TCP 或数据库协议连接。
- 数据库后端连接:代理与某个数据库实例之间的实际协议连接。
如果代理只做一对一转发,那么一个前端连接通常对应一个后端连接,代理的价值主要是统一入口和路由。
如果代理支持连接复用,多个前端请求可以先后使用同一个后端连接,甚至在某些协议和事务边界下并发使用不同后端连接。此时,前端连接中的状态不能任意保留在后端连接上,否则后一个请求会观察到前一个请求留下的状态。
因此,最重要的抽象不是“代理有多少连接”,而是:
前端会话状态
后端会话状态
请求/事务是否允许在连接之间迁移
二、连接复用:减少连接数量,不等于增加数据库并发能力
2.1 为什么需要复用
数据库连接通常不是廉价对象。建立连接可能包含:
- TCP 建连;
- TLS 握手;
- 用户认证;
- 数据库进程或线程分配;
- 会话参数初始化;
- 权限和默认对象解析。
如果应用实例很多,且每个应用实例都维护较大的连接池,就可能出现:
应用实例数 × 每实例连接池上限
远大于数据库可承受后端连接数的情况。
例如:
应用实例:100 个
每实例连接池上限:20
理论前端连接:2000
数据库最大后端连接:100
代理可以把 2000 个前端连接映射到不超过 100 个后端连接。但这不意味着 2000 个请求可以同时在数据库中执行;同一时刻真正占用数据库执行资源的请求仍受后端连接数、CPU、锁和磁盘吞吐限制。
连接复用解决的是:
- 后端连接建立成本;
- 后端连接总数;
- 多个应用池造成的连接峰值;
- 空闲前端连接对数据库连接资源的占用。
它不能直接解决:
- 慢 SQL;
- 锁等待;
- 数据库 CPU 饱和;
- 事务持锁时间过长;
- 连接池配置过大导致的排队和雪崩。
2.2 三种常见复用边界
会话级复用
一个前端连接长期绑定一个后端连接:
前端 A ───── 后端 1
前端 B ───── 后端 2
优点是语义最接近直接连接数据库,以下状态通常可以保留:
- 当前事务;
- 临时表;
- 预处理语句;
- 会话变量;
- 游标;
- PostgreSQL 的 session-level advisory lock;
- MySQL 的临时表和会话变量。
代价是复用能力弱:前端连接空闲时,后端连接也可能被占用。
事务级复用
代理在事务结束后归还后端连接:
前端 A: BEGIN ─ SQL ─ COMMIT
│
▼
后端 1 归还池中
前端 B: BEGIN ─ SQL ─ COMMIT
│
▼
可能复用后端 1
一个事务内的语句必须固定在同一个后端连接上,否则事务上下文无法成立;事务结束后才允许迁移。
这种模式能显著提高复用率,但要求事务外的会话状态不可依赖,或者代理能够正确重建状态。
典型风险包括:
-- PostgreSQL
SET search_path TO tenant_a;
CREATE TEMP TABLE t(x int);
PREPARE q AS SELECT * FROM t;
-- MySQL
SET @tenant_id = 10;
CREATE TEMPORARY TABLE t(x INT);
如果这些操作发生在事务外,后续请求可能被分配到另一个后端连接,从而观察不到预期状态。即使操作在事务内,事务结束后临时表等会话对象仍然可能继续存在于后端连接上;代理若再次把该连接分给别的租户,就会产生状态污染或信息泄漏。
语句级复用
每条语句执行前临时取得后端连接,语句结束后归还。
这种模式只适用于能够证明每条语句独立、没有事务上下文依赖的场景。事务、多语句过程、游标和会话状态都使它变得危险。
不能把“一个 SQL 请求”简单理解成“一条 SQL 字符串”。数据库协议可能包含:
- 解析;
- 绑定参数;
- 执行;
- 读取结果;
- 关闭门户或语句。
代理如果错误地在这些阶段之间切换后端连接,协议和语义都可能被破坏。
2.3 连接复用的必要条件
设一个请求包含后端可观察状态集合 。如果请求结束后状态仍可能影响后续请求,则不能安全复用,除非代理能够把状态恢复到标准值。
可安全复用的充分条件可以写成:
其中:
- 是请求完成后的后端会话状态;
- 是代理约定的初始状态。
如果不满足这个条件,则需要:
- 保持前端与后端绑定;
- 在释放连接前执行清理;
- 在分配连接时重新设置全部状态;
- 禁止依赖该状态的功能。
仅执行一个通用的 ROLLBACK 并不能清除所有会话状态。它通常只能结束当前事务,不能自动删除临时表、撤销所有会话变量、释放所有会话级锁或恢复所有会话参数。
2.4 连接池容量与排队
设:
- :可用后端连接数;
- :平均每个后端连接的完成速率;
- :请求到达速率。
理论上,稳定运行至少要求:
如果事务平均持有后端连接时间为 ,则单个后端连接的平均服务率近似为:
因此:
例如,事务平均持有后端连接 20 ms,即 秒;到达率为每秒 3000 个事务:
这只是容量估算,不是性能保证。锁等待、事务分布不均、长尾延迟和数据库内部资源都会使实际需要更高的余量。
当 个后端连接全部忙碌时,新请求只能:
- 在代理队列等待;
- 被代理拒绝;
- 让应用连接池继续等待;
- 触发上游超时。
如果应用、代理和数据库各自都有排队,就会形成多层排队。总延迟大致是:
排队超时后再无脑重试,会把原本一个请求变成多个数据库请求,形成重试风暴。
三、读写路由:语法分类只是开始
3.1 路由的基本目标
读写路由通常希望实现:
写操作、强一致读 ──> 主库
允许延迟的读 ──> 只读副本
但“读”不等于“可以去副本”。
一个查询是否能发往副本,至少取决于:
- SQL 是否修改数据;
- 当前事务是否已经包含写操作;
- 查询是否要求读到刚刚提交的数据;
- 副本是否已经追上主库;
- 副本是否处于可服务状态;
- 查询是否依赖会话状态或临时对象。
3.2 SQL 分类的限制
代理可以通过协议消息、SQL 解析器或关键字进行分类。例如:
SELECT * FROM orders WHERE id = 10;
INSERT INTO orders(id, amount) VALUES (10, 99);
UPDATE orders SET amount = 100 WHERE id = 10;
显然,INSERT 和 UPDATE 不能发往只读副本。但以下语句不能只靠第一个关键字判断:
WITH changed AS (
UPDATE orders
SET amount = amount + 1
WHERE id = 10
RETURNING *
)
SELECT * FROM changed;
在 PostgreSQL 中,数据修改语句可以出现在 WITH 中。存储过程、函数、触发器也可能修改数据:
SELECT process_order(10);
仅看外层是 SELECT,不能证明它是只读操作。
此外,某些读操作有副作用或会依赖事务锁定:
SELECT * FROM orders WHERE id = 10 FOR UPDATE;
它不是普通只读查询,必须在允许加锁的主库上执行。
因此,路由器通常采用以下策略之一:
- 只把明确安全的查询发往副本;
- 遇到无法确定的 SQL 默认发主库;
- 要求应用显式声明读一致性等级;
- 使用事务标记或连接属性控制路由。
默认发主库牺牲部分读扩展性,但更容易保证正确性。
3.3 事务是路由的基本边界
考虑以下 PostgreSQL 事务:
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
SELECT balance
FROM accounts
WHERE id = 1;
COMMIT;
第二条 SELECT 必须看到同一事务中自己的更新。即使路由器知道它是 SELECT,也不能把它切到副本。
事务的路由状态可以表示为:
空闲
│ BEGIN
▼
事务已开始、尚未写入
│ 发现写操作
▼
事务已写入,固定主库
│ COMMIT / ROLLBACK
▼
空闲
一旦事务中执行了写操作,后续语句通常应固定在同一个主库连接上,直到 COMMIT 或 ROLLBACK。
即便事务只读,也不能自动认为副本可用。PostgreSQL 和 MySQL 的事务隔离、快照建立时机以及副本回放状态都必须纳入判断。
3.4 复制延迟造成的读后写不一致
设:
t0: 客户端向主库写入订单 order_id=10
t1: 主库返回 COMMIT 成功
t2: 复制日志尚未回放到副本
t3: 客户端向副本查询 order_id=10
如果 ,查询可能返回“不存在”。
这不是数据库违反了主库事务提交语义,而是客户端先后访问了两个不同的复制状态:
常见解决办法有三类。
会话粘滞
写成功后,在一段时间内把该客户端的读路由到主库。
它简单,但“时间窗口”只是经验参数:如果副本延迟超过窗口,问题仍会出现。
复制位置等待
主库提交后返回一个可表示提交位置的标记;读副本前,等待副本回放到至少该位置。
抽象条件为:
这是比固定时间等待更准确的方法,但要求数据库、复制协议和代理能够暴露并处理对应位置。
应用声明一致性要求
例如:
强一致读:主库或已确认追平的副本
最终一致读:任意健康副本
写后读:携带上一次写入的位置要求
这比试图从 SQL 猜测业务意图更可靠。
3.5 一个常见反例
请求 A:
INSERT user(id=1)
COMMIT
请求 B:
SELECT user WHERE id=1
如果请求 A 和 B 使用不同的应用线程、不同的前端连接,且代理只做“写主库、读副本”的语句分类,那么 B 很可能被路由到尚未追平的副本。
“同一个用户”或“同一个 HTTP 会话”对数据库并不可见,除非网关显式维护粘滞状态或应用传递一致性标记。
四、分片:路由键决定可扩展性,也决定查询边界
4.1 什么是分片
分片(sharding)是把一个逻辑数据集拆到多个独立数据库或数据库实例上。每个实例保存完整数据的一个子集。
设分片函数为:
其中:
- 是分片键;
- 是哈希函数;
- 是分片数量;
- 是目标分片编号。
例如以 user_id 分片,四个分片的路由为:
user_id=10: 10 mod 4 = 2 -> shard_2
user_id=11: 11 mod 4 = 3 -> shard_3
代理必须从 SQL 参数、协议绑定参数或应用元数据中取得 。如果查询没有分片键:
SELECT * FROM orders WHERE status = 'PAID';
代理无法确定唯一目标,只能:
- 广播到所有分片;
- 由路由表推断;
- 拒绝查询;
- 发往包含全局索引的专用节点。
广播查询的结果需要在代理层合并,可能涉及排序、分页、聚合和去重。
4.2 分片键不是普通索引列
良好的分片键需要同时满足:
- 查询经常带上它;
- 数据分布较均匀;
- 相关事务尽量落在同一分片;
- 生命周期和迁移策略可接受;
- 不会形成单个热点分片。
例如,以 tenant_id 分片通常有利于租户隔离,但若一个租户远大于其他租户,会造成热点。以递增时间分片可能使最新分片成为写热点。
4.3 跨分片查询的执行过程
假设按 user_id 分四片,执行:
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE user_id IN (10, 11, 12)
GROUP BY user_id;
代理可以先计算:
10 -> shard_2
11 -> shard_3
12 -> shard_0
然后改写为三个子查询:
-- shard_2
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE user_id IN (10)
GROUP BY user_id;
-- shard_3
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE user_id IN (11)
GROUP BY user_id;
-- shard_0
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE user_id IN (12)
GROUP BY user_id;
最后代理合并结果。
对于 SUM,可以先分片局部聚合,再做全局求和;但并非所有 SQL 都能这样安全改写。
COUNT(*)
可以把各片结果相加:
MIN 和 MAX
可以取各片局部结果的最小值或最大值。
AVG
不能直接平均各片的平均值,除非每片行数相同。正确做法是同时汇总:
ORDER BY ... LIMIT
各分片只取全局 LIMIT 行可能错误。正确策略通常是各片取足够的候选集,再在代理端合并排序;但候选数量、内存和网络开销会增加。
4.4 分片事务的边界
单分片事务可以保持普通数据库事务语义:
BEGIN;
UPDATE orders SET amount = 100 WHERE user_id = 10 AND order_id = 1;
INSERT INTO audit_log(...) VALUES (...);
COMMIT;
前提是两张表的相关行都位于同一分片。
跨分片事务则不同:
shard_0: 扣减库存
shard_1: 创建订单
如果 shard_0 成功、shard_1 失败,普通的本地 COMMIT 无法自动回滚另一片。
分布式事务通常需要协调者和两阶段提交:
协调者 -> 所有参与者:PREPARE
参与者 -> 协调者:prepared
协调者 -> 所有参与者:COMMIT
故障时可能出现参与者已经 prepared、但尚未收到最终 COMMIT 的状态。协调者日志、超时恢复和参与者查询都变得必要。
如果业务允许,常见替代方案是:
- 把强一致操作设计在同一分片;
- 使用消息和补偿事务;
- 使用幂等状态机;
- 接受暂时不一致,再异步收敛。
这里不能把“代理同时向多个分片发 SQL”称为事务。并发发送只解决执行效率,不自动提供原子性。
4.5 扩容与分片函数变化
直接把 改成 会导致大量键迁移:
旧:h(k) mod 4
新:h(k) mod 8
绝大多数键的目标都可能改变。扩容通常需要:
- 建立新分片;
- 确定迁移范围;
- 复制或导出数据;
- 处理迁移期间的增量写入;
- 校验行数、校验和或业务结果;
- 切换路由;
- 保留回滚或旧路由能力。
一致性哈希、虚拟槽位和路由表可以降低一次迁移的范围,但不能消除迁移期间的双写、读旧数据、重复写入和切换失败问题。
五、审计:记录“谁做了什么”,而不是只保存 SQL 字符串
审计的目标通常包括:
- 追踪身份;
- 记录访问对象;
- 记录操作时间和结果;
- 支持合规调查;
- 发现异常访问;
- 分析高风险 SQL。
一条可用的审计事件至少应区分:
真实用户身份
应用服务身份
代理身份
数据库登录身份
租户身份
请求 ID / Trace ID
客户端地址
目标实例和分片
SQL 模板
绑定参数或参数摘要
事务标识
执行结果
耗时、返回行数、错误码
5.1 代理审计与数据库审计不是同一件事
代理能看到应用发来的协议请求,但看不到所有数据库内部行为。例如:
SELECT process_order(10);
代理可能只记录这条调用;函数内部执行了哪些 SQL,只有数据库侧审计或数据库日志才可能完整记录。
同样,数据库内部还可能产生:
- 触发器执行;
- 级联删除;
- 后台任务;
- 管理员直接连接;
- 复制线程;
- 数据库自身的 DDL 或维护操作。
因此完整审计通常需要分层:
应用审计:业务动作和真实用户
代理审计:入口、路由、协议请求
数据库审计:实际数据库操作和内部行为
基础设施审计:实例、网络和运维操作
5.2 参数与隐私
只记录 SQL 模板:
SELECT * FROM users WHERE email = ?;
有利于聚合分析,但无法直接还原具体访问对象。完整记录参数又可能把密码、令牌、身份证号等敏感信息写入日志。
可采用:
- 对敏感参数脱敏;
- 对参数做不可逆摘要;
- 对高风险字段按权限加密;
- 单独记录参数是否存在而非具体值;
- 让应用提供业务对象 ID 和请求 ID。
审计日志本身是敏感数据,必须有访问控制、保留周期、完整性保护和容量规划。开启审计后,日志写入、序列化和网络传输也会增加延迟;不能在高峰期无评估地记录完整结果集。
六、认证、权限和租户隔离
代理前端认证成功,不等于后端数据库已经完成正确的权限判断。
6.1 两种身份模型
统一数据库身份
所有应用请求使用一个数据库用户:
用户 A ─┐
用户 B ─┼─> proxy ─> db_user_app
用户 C ─┘
优点是连接管理简单;缺点是数据库只能看到统一身份,租户权限必须由代理或应用保证。
身份透传
不同用户或服务使用不同数据库身份,或者代理把身份映射到数据库可识别的会话属性。
这样数据库权限更细,但连接复用要求更高:后端连接归还前必须清理身份相关状态,且代理不能把前一个身份的会话状态带给后一个身份。
6.2 租户隔离的必要条件
以下做法不能单独构成可靠隔离:
SELECT * FROM orders WHERE tenant_id = 10;
如果 SQL 是应用拼接的,存在注入或漏加条件的风险。更强的方案包括:
- 数据库原生行级安全机制;
- 每租户独立数据库身份;
- 每租户独立分片;
- 代理强制解析并注入条件;
- 应用层与数据库层双重校验。
代理改写 SQL 必须考虑嵌套查询、CTE、别名、视图、函数和参数绑定。简单的字符串替换很容易漏掉路径或改变语义。
七、故障边界:代理不是数据库高可用的替代品
增加代理后,系统至少多出以下故障边界:
应用 -> 代理
代理内部队列和路由状态
代理 -> 数据库
主库 -> 副本复制链路
分片之间的协调关系
每条边界都可能独立失败。
7.1 连接建立失败与请求执行失败
必须区分:
请求尚未到达数据库
例如代理在排队时断开、连接池获取失败。此时通常没有数据库副作用,重试相对安全。
请求已发送但结果丢失
例如数据库已经提交事务,代理在返回响应前崩溃:
代理 -> 数据库:COMMIT
数据库:提交成功
数据库 -> 代理:响应
代理:在收到响应前断开
客户端:只看到连接错误
客户端无法仅根据连接错误判断事务是否提交。此时无脑重试可能造成重复扣款、重复创建订单等副作用。
安全做法是使用业务幂等键:
INSERT INTO payments(request_id, user_id, amount)
VALUES ('req-001', 10, 100);
并对 request_id 建立唯一约束。重试时:
- 若第一次未提交,第二次成功插入;
- 若第一次已提交,第二次触发唯一约束,应用查询原记录并返回相同业务结果。
幂等键必须由数据库约束或等价的原子机制保护,不能只依赖应用先查询再插入,因为并发下会产生竞态。
7.2 断线重连不能自动恢复事务
以下事务执行到一半时连接断开:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 连接断开
COMMIT;
客户端无法假设事务已回滚,也无法假设更新已提交。不同数据库和断线时机下,服务器可能尚未收到语句、已执行但未提交,或已经提交而响应丢失。
代理可以自动重连并重放某些只读请求,但对写请求和事务不能仅凭网络错误重放。
7.3 主库故障转移
读写路由中的故障转移通常包含:
- 健康检查发现主库不可用;
- 判断副本是否足够新;
- 选举或指定新主库;
- 更新代理路由状态;
- 让旧主库不能继续接受写入;
- 让客户端重新建立连接。
第 5 步是防止脑裂的关键。若旧主库网络隔离但仍认为自己是主库,代理把一部分写请求发给旧主库、另一部分发给新主库,就会产生分叉数据。
健康检查也不能只检查 TCP 端口。一个实例可能端口可连接,但:
- 数据库无法执行新事务;
- 磁盘已满;
- 复制严重落后;
- 只读状态不符合预期;
- 锁或连接资源耗尽。
路由健康应至少包含角色、可写性、复制进度和实际查询能力。
7.4 代理自身的高可用
单个代理成为所有应用的唯一入口后,它就是新的单点。常见部署是多个无状态代理实例:
应用
│
├── proxy-1
├── proxy-2
└── proxy-3
│
└── 数据库集群
但“无状态”不是绝对的。代理可能保存:
- 后端连接池;
- 路由缓存;
- 主从角色状态;
- 分片元数据;
- 会话粘滞信息;
- 审计缓冲区;
- 分布式事务协调状态。
代理进程重启通常会使前端连接和后端连接同时断开。应用必须能够重新获取连接;未完成事务不应被假定为可恢复。
八、诊断:先定位在哪一层排队或失效
遇到“数据库慢”时,不能只看数据库 CPU。应沿请求路径分层观察。
8.1 连接层指标
关注:
前端连接数
后端连接数
后端连接池使用率
等待可用后端连接的请求数
连接建立速率
连接失败数
空闲连接数量
事务中连接数量
如果前端连接很多、后端连接很少,但代理队列很长,问题可能是后端容量不足,而不是连接复用失效。
如果后端连接很多、数据库 CPU 和锁等待很高,继续增大连接池通常会使问题更严重。
8.2 路由层指标
需要记录或统计:
主库请求数
副本请求数
广播查询数
无法解析的 SQL 数
因事务固定主库的请求数
副本延迟
路由切换次数
路由错误和回退次数
尤其要识别“读请求被发送到副本,但业务要求读后写一致”的情况。
8.3 数据库层验证
在 PostgreSQL 中,可以查看当前连接和活动查询:
SELECT pid,
usename,
application_name,
client_addr,
state,
wait_event_type,
wait_event,
query
FROM pg_stat_activity;
在 MySQL 8.4 中,可以查看线程和当前语句:
SHOW FULL PROCESSLIST;
这些命令的结果只能说明数据库当前看到的连接和查询。若代理采用事务级复用,数据库看到的用户、客户端连接和应用逻辑请求可能不是一一对应关系,因此应通过连接初始化信息、请求 ID 或代理日志建立关联。
8.4 审计与慢查询的交叉验证
一个请求耗时很长,至少要区分:
等待应用连接池
等待代理后端连接
等待数据库锁
数据库实际执行
等待结果传输
代理审计写入
若数据库日志显示执行时间很短、应用却超时,问题可能在代理队列、网络或结果传输。若代理记录 SQL 已发送但没有返回,应结合数据库活动查询判断它是在执行、等待锁,还是已经结束但响应链路失败。
九、几个容易混淆的结论
“代理池越大越好”
错误。后端连接过多会增加:
- 内存和进程/线程开销;
- 上下文切换;
- 锁竞争;
- CPU 调度压力;
- 故障时同时重连的冲击。
容量应由数据库可承受的并发、事务持有时间和排队目标共同决定。
“SELECT 都可以去副本”
错误。SELECT 可能:
- 调用写函数;
- 使用
FOR UPDATE; - 依赖刚提交的数据;
- 依赖当前事务中的写入;
- 依赖尚未同步的会话状态。
“连接池已经保证事务安全”
错误。连接池只能管理连接资源。事务安全还要求:
- 事务边界明确;
- 一个事务不跨后端连接;
- 连接释放前没有未提交事务;
- 会话状态不会污染后续请求;
- 驱动和代理对预处理协议的支持一致。
“分片后每个 SQL 都还能像单库一样执行”
错误。跨分片的连接、事务、排序、聚合、唯一约束和外键都需要重新定义。代理可以隐藏部分路由细节,但不能凭空创造全局索引和全局原子性。
“代理审计已经覆盖所有数据库行为”
错误。代理看到的是入口请求;函数、触发器、管理员直连和数据库内部任务需要数据库侧审计或日志补充。
“连接断开后重试即可”
错误。对于只读、幂等或明确未发送的请求,重试相对安全;对于提交结果未知的写事务,必须使用幂等设计、业务查询或人工恢复流程。
十、如何划分代理的职责
一个可验证的边界通常如下:
应用:
事务边界、业务幂等、读一致性要求、分片键提供
代理/网关:
连接复用、连接数控制、明确规则下的路由、入口审计、限流
数据库:
约束、事务、锁、权限、数据落盘、复制和恢复
复制/集群系统:
角色管理、故障转移、复制进度、脑裂防护
运维系统:
配置发布、监控、审计保留、备份和演练
代理适合执行“机械且可验证”的策略,例如:
- 根据显式分片键路由;
- 根据事务状态固定主库;
- 拒绝没有分片键的高风险查询;
- 限制单租户连接数;
- 记录请求和路由结果;
- 在明确的错误条件下摘除故障节点。
代理不适合承担无法证明的推断,例如:
- 从
SELECT关键字推断绝对只读; - 从网络断开推断事务未提交;
- 从某个副本 TCP 可连接推断它已经追平;
- 用字符串替换实现复杂 SQL 的安全改写;
- 用一个本地事务模拟跨分片原子事务。
数据库代理与网关的核心价值,是把连接、路由、治理和故障处理集中到一个可观测入口;它的核心风险,也正来自这里:原本由应用直接面对数据库的状态、时序和错误,现在被代理重新组合。只有明确前端会话、后端会话、事务、复制位置、分片参与者和提交结果的边界,连接复用、读写路由、分片与审计才能在不改变数据正确性的前提下发挥作用。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:分布式数据库中的时间与 ID:时钟、序列、Snowflake、UUID 和顺序
- 下一篇:云托管数据库选型:责任边界、高可用、扩缩容、成本和退出策略
- 延伸:数据库连接与连接池:容量、超时、排队、泄漏和故障恢复
- 延伸:数据库复制、分片与高可用:一致性、路由、故障转移和扩容
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论