让一次锁等待有时间上限的可配参数

事务拿锁时别的请求会礼貌等待

等待锁可能是正常协调,也可能让连接和请求持续排队。PostgreSQL 的 lock_timeout 可以让调用方停止等待,但它不会解释持锁者为什么慢,更不会自动决定业务是否可以重试。应从操作能够接受的延迟出发设定预算,同时找到实际阻塞者;单纯延长超时或增加尝试次数,可能只是扩大同一条等待队列。 坏掉的样子 锁超时分别作用于每一次锁获取尝试,包括语句隐式申请的锁。它测量的是等待当前锁的时间,而不是整条语句的总运行时间。查询已经执行了一段时间,随后又申请一把锁,仍然可能触发这个限制。反过来,多次较短的等待也可能累计超过一个配置间隔。把锁超时理解成“只有语句刚开始时检查一次”,或把它当成整次查询的总截止时间,都会漏掉真实的等待范围。 statement_timeout 限制整条语句执行,其中也包括等锁。如果它更短,可能先于锁超时取消语句。空闲事务会话超时处理的则是另一种状态:事务还开着,但服务器正在等待客户端发下一条命令。卡在更新内部等待锁的后端通常仍是活动状态,等待事件属于锁;不能看到连接排队,就直接把所有等待者叫作空闲事务。真正闲着不提交的持锁者和正在等待的受阻者,需要分别定位。 快速验证 准备同一个 PostgreSQL 18 可丢弃数据库,开启甲、乙两个全新的 psql -X -v ON_ERROR_STOP=1 会话。实验使用默认的零值锁超时,且没有其他会提前干预的超时设置。下面的毫秒参数只用于快速复现,不是生产建议值。甲先建立独立模式和计数表: CREATE SCHEMA timeout_demo; CREATE TABLE timeout_demo.counter (id integer PRIMARY KEY,n integer); INSERT INTO timeout_demo.counter VALUES (1,0); 甲在显式事务中把数字改成十,暂时不提交,也不回滚,以便继续持有目标行的锁: BEGIN; UPDATE timeout_demo.counter SET n=10 WHERE id=1; 乙开启事务,对同一行加一,并给取锁等待一百毫秒预算。为展示预期错误及后续事务状态,只在这一小段临时关闭遇错停止: ...

二月 16, 2028 · 1 分钟 · XB Field Notes
观察、决策、行动三步示意图,旁边是限制查询并检查恢复的英文标题。

查询超时给坏查询的代价上限

查询超时限制的是数据库工作中的某个阶段,不会自动覆盖连接池排队,也不保证远端已经取消执行或事务清理已经结束。要保护共享容量,必须说清由哪个组件计时、它到点会中断什么,以及错误返回之后还剩下什么状态。只有一个名字叫 timeout 的配置,不能构成完整的成本边界。 说清这只钟从何时开始,管哪段工作 PostgreSQL 的 statement_timeout 从命令到达服务器开始计时,直到服务器完成处理。它不是调用方整个请求的期限:此前获取连接池借用的等待,以及服务器完成之后客户端本地处理结果的时间,不在同一计时范围内。锁等待也可能消耗语句时间,因此“还没有真正开始做有用的查询工作”,不代表语句预算尚未开始使用。 lock_timeout 的含义更窄,分别约束每次尝试获取锁的等待,而语句超时也可以中断不在等锁的执行工作。事务中空闲会话的超时又是另一条边界:它针对已经打开事务、却正在等待客户端下一条查询的会话,并不是当前仍在执行的语句。把这些数值设成一样,并不会让它们获得相同作用;应先理解它们观察的是哪一种状态。 驱动参数也需要同样认真地阅读。Python 的 sqlite3.connect(timeout=...) 设置的是遇到数据库锁时最多等多久,不是所有 SQL 的执行总期限。不同驱动里都叫“超时”的参数,可能分别针对建连、锁等待或读取;只复制名称和数值,可能让真正昂贵的查询仍然完全没有执行时间限制。 坏掉的样子 调用方停止等待时,数据库仍可能继续执行。即便 PostgreSQL 的取消请求已经成功发出,也不能保证它实际改变了什么,因为原命令可能早已处理完成。需要观察命令结果与事务结果,不能把“捕获到超时异常”或“发出了 cancel”直接当成所有远端工作均已停止的证明,更不能据此断定是否可以安全重放有副作用的操作。 语句时间限制也不会退还已经花掉的资源。清理、事务恢复仍可能耗时,先前的操作也可能已有各自结果。连接归还池中之前,应完成所需恢复并核对状态,而不是在异常被捕获后立刻当作干净连接继续借出。如果报表契约要求完整结果,半份结果就不能因为“总比没有好”而自动变成成功,除非产品本来就明确允许这种部分结果。 阈值应从工作负载和请求预算出发,同时给连接获取、响应传输与恢复留下空间。把 p99 简单乘固定倍数,不是普遍成立的安全规则:百分位描述的是已经观察到的时长,并不能定义哪些业务请求本来就合理。PostgreSQL 文档也提醒,不应不加区分地在全局配置语句超时,因为那会影响所有会话;交互查询与明确安排的长时分析任务可能需要不同策略。 快速验证 把代码保存为 query_probe.py,运行 python3 query_probe.py。示例只使用标准库中的 SQLite,本次环境是 Python 3.14.4 和 SQLite 3.53.0。两条连接共享脚本自己的临时数据库文件,不连接 PostgreSQL,也不使用生产凭据。短时长与回调频率用于快速展示边界,不是推荐直接上线的默认参数。 from contextlib import closing from pathlib import Path import sqlite3 from tempfile import TemporaryDirectory import time def exercise(): with TemporaryDirectory(prefix="query-budget-") as directory: database = Path(directory) / "probe.db" with ( closing(sqlite3.connect(database, timeout=0.02, isolation_level=None)) as connection, closing(sqlite3.connect(database, timeout=0.02, isolation_level=None)) as contender, ): connection.execute("CREATE TABLE ledger(id INTEGER PRIMARY KEY)") connection.execute("INSERT INTO ledger VALUES (1)") connection.execute("BEGIN IMMEDIATE") try: contender.execute("INSERT INTO ledger VALUES (3)") except sqlite3.OperationalError as error: assert error.sqlite_errorcode == sqlite3.SQLITE_BUSY else: raise AssertionError("the held writer lock must prevent this insert") finally: connection.rollback() print("busy timeout rejects a competing writer; row 3 was not inserted") connection.execute("BEGIN") connection.execute("INSERT INTO ledger VALUES (2)") callbacks = 0 started = time.monotonic() deadline = started + 0.05 def expired(): nonlocal callbacks callbacks += 1 return int(time.monotonic() >= deadline) connection.set_progress_handler(expired, 1000) try: connection.execute(""" WITH RECURSIVE numbers(n) AS ( VALUES(1) UNION ALL SELECT n + 1 FROM numbers WHERE n < 100000000 ) SELECT sum(n) FROM numbers """).fetchone() except sqlite3.OperationalError as error: assert error.sqlite_errorcode == sqlite3.SQLITE_INTERRUPT else: raise AssertionError("the expensive query must reach its progress deadline") finally: connection.set_progress_handler(None, 0) connection.rollback() elapsed = time.monotonic() - started assert callbacks > 0 and elapsed >= 0.05 rows = connection.execute("SELECT id FROM ledger ORDER BY id").fetchall() assert rows == [(1,)] assert connection.execute("SELECT 42").fetchone() == (42,) assert not connection.in_transaction print("progress deadline interrupts SQL after", round(elapsed, 3), "seconds") print("explicit rollback leaves only committed row 1; the connection remains usable") print("SQLite", sqlite3.sqlite_version) exercise() 第一条连接先持有写锁,第二条连接因此无法插入编号三的记录,在锁等待额度耗尽后得到 SQLITE_BUSY。随后回滚第一条事务,清除刻意制造的竞争。这是在真实 SQLite 锁上检查连接 timeout 的含义,不是直接抛出一个模拟驱动异常,再声称数据库已经按期终止了查询。 ...

九月 2, 2026 · 2 分钟 · XB Field Notes