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

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

等待锁可能是正常协调,也可能让连接和请求持续排队。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
WATCH 保护依赖读取结果的事务,EXEC 无穿插地执行通过检查的命令队列。

MULTI EXEC 把一串命令当作一次

MULTI/EXEC 能阻止别的客户端命令插进队列的执行过程,却不会自动保护应用此前已经读到的值。如果两个购买方先读到相同库存,再各自写回计算结果,即使每次 SET 都放进事务,也仍然可能丢失一次更新。问题出在读取与提交之间的空隙,而不是 SET 执行得是否足够连续。 先监视,再读取依赖 WATCH 让 EXEC 变成有条件的执行:被监视的键从建立监视之后不能发生修改。因此,必须先 WATCH,再读取参与决策的数据。若键在提交前改变,Redis 拒绝整个队列并返回空响应;这只是不执行自己的队列,不会撤销竞争方已经完成的修改。 监视与事务状态都属于同一条连接。redis-py 流水线在 watch 之后保留连接,multi 之前的 GET 会立即发出,应用可以读取结果并做判断;multi 之后才转为缓冲命令,等待 execute。用上下文管理器包住一次尝试,可以在结束时重置流水线状态并释放专用连接。 凡是会影响决策的值或“不存在”条件,都要纳入监视范围。只监视库存,却忽略另一个键保存的额度,并不能保护额度判断。WATCH 也不只是比较最终字节:即便 SET 写回相同内容,也可以使监视失效;原来不存在的被监视键被创建,同样会导致冲突。本次版本中,由过期和内存淘汰引起的变化也计算在内。 重试整个决策,而不是旧计算结果 收到明确的冲突结果后,应建立新监视,重新读取依赖,再检查业务条件和计算写入值。继续使用旧库存,相当于把同一个错误判断再执行一次。EXEC 无论成功还是因监视冲突取消,都会清除监视;如果应用读完后决定放弃,也可以通过 UNWATCH 主动解除。 重试需要次数或时间上限,并让调用方知道何时已耗尽预算。热点键上的大量冲突会放大成本,即使最终没有丢数据也会增加延迟。退避可以缓和竞争,却补不上遗漏的监视键。若原生命令已经能表达所需的原子操作,应优先使用它;实验刻意拆开库存算术,是为了展示旧值覆盖问题。 监视检查通过,也不会让事务获得自动回滚能力。EXEC 内部某一条命令运行失败,其它成功命令仍可能已经生效。应校验相关数据模型并逐项检查响应,不要把数个互不相干的副作用放在一起,就称为业务上的全成全败。 坏掉的样子 先读后 WATCH,会漏掉建立监视前的那段竞争窗口。用一条连接监视、另一条连接提交,不会保护后者的队列。在重试循环中发邮件或发起支付,则可能在 Redis 拒绝本次尝试之前,就已经产生了无法随队列取消的外部效果。 还必须区分服务端明确报告的冲突与传输故障。redis-py 8.1.0 在监视期间遇到连接异常,也可能抛出 WatchError;仅凭异常类型,不能断言 EXEC 从未执行。提交之后应答丢失,需要核对业务结果或依赖幂等协议,不能无条件重新执行不可逆动作。 ...

七月 22, 2027 · 3 分钟 · XB Field Notes
Redis 先排队,再在隔离窗口内执行命令;错误不代表自动撤销已完成写入。

Redis 事务把一组命令当作一个整体执行

Redis 事务先在一条连接上积累命令队列,再进入一个隔离的执行窗口。MULTI 开始排队,EXEC 按顺序执行已接受的命令。这里的关键保证是其它客户端的命令不能插进执行过程,而不是后面一条失败时,前面所有成功写入都能自动撤销。 入队与执行是两个阶段 QUEUED 表示服务端接受了命令,等待后续执行,并不表示这条命令想完成的写入已经成功。Redis 在接受命令时就会检查命令是否存在、参数数量是否正确;依赖当前数据的错误,例如向字符串键执行列表操作,则可能直到 EXEC 才暴露。不能笼统地把它们都称为语法错误,因为两种失败对应的队列处理方式并不相同。 在队列中发送 GET,同样先收到 QUEUED,真正的值出现在 EXEC 的结果数组里。客户端不能先读取这个排队 GET 的实际结果,再据此决定本次事务后面的命令。需要在应用内判断时,可先用 WATCH 保护读取依赖;必须在读取与写入之间分支时,可考虑有界的服务端脚本。 客户端缓冲又增加了一层。redis-py 的普通事务流水线先把命令放进本地列表,直到 execute 才发送 MULTI、各条命令与 EXEC。set 方法返回流水线对象,并不等于服务端已确认入队。下面使用专用连接读取原始响应,是为了观察协议的实际阶段,而不是把客户端的链式调用误当成服务器执行结果。 隔离从执行时开始 MULTI 不会替调用方提前锁住相关键。一个客户端仍在构造队列时,另一个客户端完全可以更新这些键。队列中的 GET 读取的是它执行时的值,不是 MULTI 开始时保存的快照;后面的 GET 也能看到本事务前面 SET 的写入。把不被插队说成启动时的一致快照,会误导依赖旧值的业务判断。 事务必须始终使用同一条连接,而且这条连接不能混入无关业务。如果连接池为 MULTI 和 EXEC 分别分配连接,它们就不属于同一个事务状态。应用代码应优先使用客户端提供的事务抽象;实验直接访问连接,只用于核验已固定版本的协议行为。 ...

三月 31, 2027 · 3 分钟 · XB Field Notes
一种让事务从头开始的并发信号。

事务可能失败,剩下的就靠你重试

PostgreSQL 中,另一个事务修改了同一行,不代表当前事务必然失败。结果取决于隔离级别、语句怎样使用数据,以及竞争者最终提交了什么。把所有碰撞都归成“等一下就好”,会掩盖无效输入,也可能重复使用已经过时的业务决策。需要定义的重试单位,是包含相关读取、判断与写入的一次完整操作。 坏掉的样子 已提交读下,普通查询使用本条语句的快照。更新遇到竞争写者时,可以先等待,再针对对方提交后的行版本重新检查条件。因此,两个把字段加一的更新完全可能先后成功。另一个常见写法却不同:客户端先读取数字,在内存里算好绝对值,稍后再把那个常量写回。数据库不会自动替客户端重算这段业务逻辑,也不能把这两种写法混为一谈。 可重复读在事务第一条非事务控制语句处建立快照,随后通常沿用。如果目标行在这个快照之后被别人修改并提交,当前事务尝试更新时就可能立即收到序列化失败。可串行化还会识别跨不同行形成的依赖异常。它们都没有承诺“只有提交才报错”,隔离更强也不等于每个负载都会更早、更频繁地失败。执行语句与提交事务这两个位置都必须检查结果。 诊断时应记录第一次错误的 SQLSTATE、隔离级别和操作标识。后续查询出现的失败事务状态只是前面错误的结果;如果日志只留下最后一条查询,就可能把真正导致中止的原因覆盖掉。 快速验证 准备 PostgreSQL 18 的可丢弃数据库,用两个全新的 psql -X -v ON_ERROR_STOP=1 连接,分别记作甲和乙。严格按下面的先后顺序执行,未明确开启事务时,乙保持自动提交。甲先建立独立实验模式和计数器,不要把示例放进生产库: CREATE SCHEMA conflict_demo; CREATE TABLE conflict_demo.counter (id integer PRIMARY KEY,n integer); INSERT INTO conflict_demo.counter VALUES (1,0); 甲开启已提交读事务并读取零。读取完成后先不要提交,让乙获得插入调度的机会: BEGIN ISOLATION LEVEL READ COMMITTED; SELECT n FROM conflict_demo.counter WHERE id=1; 乙把数字加一并提交,返回一: ...

二月 1, 2027 · 1 分钟 · XB Field Notes
一个事务先中止,再在上限内重新开始。

有界重试给事务一次重新执行的机会

重试可以帮助恢复因为并发而被拒绝的事务,但不能保证成功,也不会自动让外部副作用变成幂等。重试单位应包含决定写入内容的所有数据库读取与应用判断。如果新事务仍使用第一次快照算出的金额或目标值,即使执行了新的开始事务命令,也没有真正重新做出适合当前数据的决策。 坏掉的样子 PostgreSQL 用 40001 表示序列化失败,用 40P01 表示死锁。收到这些明确拒绝后,可以在回滚并检查剩余预算的前提下再次尝试完整操作。唯一约束错误却可能是永久的:同一个无效标识不会因为多等一会儿就变成合法输入。某些键值确实由此前查询结果决定,需要特定业务的重算策略,不能据此将所有异常都塞进统一重试循环。 错误既可能发生在语句执行期间,也可能出现在提交阶段。服务器明确拒绝事务,与客户端等待提交确认时连接断开,是两种不同情况。后一种可能已经提交,只是确认没有传回来;盲目重放就可能重复操作。在数据库事务之外已经完成的支付、发信等动作,也不会随着数据库回滚自动撤销。循环本身既不能回收这些副作用,也不能凭空识别重复。 快速验证 准备 Linux 或 macOS 上的 Python 3,以及可通过命令路径运行的 PostgreSQL 18 客户端 psql。设置 PGDATABASE、PGHOST 及必要连接参数,明确指向允许创建模式的可丢弃数据库。提前配置好认证,示例禁止交互式密码提示。把下面的程序保存为 retry-demo.py,再执行 python3 retry-demo.py。它创建并删除自己的实验模式;如果同名模式已经存在,初始化会失败,不能拿已有业务模式充当实验数据。 import os import random import re import selectors import subprocess import time PSQL = ['psql', '-X', '-w', '-qAt', '-v', 'ON_ERROR_STOP=1', '-v', 'VERBOSITY=sqlstate'] ENV = dict(os.environ, PGCONNECT_TIMEOUT='2') def run(sql): return subprocess.run(PSQL, input=sql, text=True, capture_output=True, check=True, timeout=5, env=ENV).stdout.strip() def read_value(worker): deadline = time.monotonic() + 5 data = bytearray() with selectors.DefaultSelector() as ready: ready.register(worker.stdout, selectors.EVENT_READ) while b'\n' not in data: remaining = deadline - time.monotonic() if remaining <= 0 or not ready.select(remaining): raise TimeoutError('Initial read was not confirmed') chunk = os.read(worker.stdout.fileno(), 4096) if not chunk: raise RuntimeError('Connection ended before initial read') data.extend(chunk) return int(data.strip()) def attempt(mode, number): worker = subprocess.Popen(PSQL, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=ENV) try: worker.stdin.write("SET statement_timeout='1s';\n" 'BEGIN ISOLATION LEVEL REPEATABLE READ;\n' 'SELECT n FROM retry_demo.counter WHERE id=1;\n') worker.stdin.flush() observed = read_value(worker) # A real second connection commits after this attempt's snapshot. if mode == 'always' or (mode == 'once' and number == 1): run('UPDATE retry_demo.counter SET n=n+1 WHERE id=1;') statement = ('INSERT INTO retry_demo.counter VALUES (1,99);' if mode == 'permanent' else f'UPDATE retry_demo.counter SET n={observed + 1} WHERE id=1;') finish = r''' \set ON_ERROR_STOP off __STATEMENT__ \if :ERROR \echo ATTEMPT :SQLSTATE ROLLBACK; \else COMMIT; \if :ERROR \echo ATTEMPT :SQLSTATE \else \echo ATTEMPT 00000 \endif \endif '''.replace('__STATEMENT__', statement) output, _ = worker.communicate(finish, timeout=5) match = re.fullmatch(r'ATTEMPT ([0-9A-Z]{5})', output.strip()) if worker.returncode != 0 or match is None: raise RuntimeError('No confirmed transaction outcome; do not replay blindly') return match[1], observed finally: if worker.poll() is None: worker.kill() worker.communicate() def increment(mode): observations = [] for number in range(1, 4): state, observed = attempt(mode, number) observations.append(observed) if state not in {'40001', '40P01'} or number == 3: return {'attempts': number, 'state': state, 'reads': observations} time.sleep(random.uniform(0, 0.01 * 2 ** (number - 1))) run('CREATE SCHEMA retry_demo; ' 'CREATE TABLE retry_demo.counter (id integer PRIMARY KEY,n integer); ' 'INSERT INTO retry_demo.counter VALUES (1,0);') try: cases = [ ('once', {'attempts': 2, 'state': '00000', 'reads': [0, 1]}, 2), ('permanent', {'attempts': 1, 'state': '23505', 'reads': [0]}, 0), ('always', {'attempts': 3, 'state': '40001', 'reads': [0, 1, 2]}, 3), ] for mode, expected, expected_value in cases: run('UPDATE retry_demo.counter SET n=0 WHERE id=1;') result = increment(mode) value = int(run('SELECT n FROM retry_demo.counter WHERE id=1;')) assert result == expected and value == expected_value, (mode, result, value) print(mode, result, 'stored', value) finally: run('DROP SCHEMA retry_demo CASCADE;') 程序使用真实的两条数据库连接。每次尝试都开启可重复读事务,先读计数值,再由应用算出要写入的绝对值。竞争连接可以在读取之后、更新之前提交一次增量,迫使原快照下的更新失败。这种调度刻意检查“重试有没有重新读取和计算”,而不只是看程序有没有再次发送更新语句。 ...

十一月 9, 2026 · 2 分钟 · XB Field Notes
一个在严格读取与并发写入之间取舍的刻度。

按业务不变量选择事务隔离

选择隔离级别,应从操作必须维持的不变量出发。一致快照很有用,但两个事务可能分别作出看似合理的决定,却在一起提交后得到错误结果。PostgreSQL 的可重复读保持事务读取快照稳定,仍允许部分序列化异常。可串行化则增加依赖检测,拒绝无法解释为任何串行执行顺序的并发组合,同时要求应用处理相应重试。关键是知道业务依赖哪项保证,而不是单纯选择一个听起来更强的名称。 稳定视图与正确决策分开判断 假设有两名值班医生,要求至少一名留岗。每名医生只有在看到可用人数大于一时才允许离开。两个可重复读事务可以都看到两人,然后分别把不同医生标记为离岗。它们没有覆盖彼此的同一行,因此都可能提交,最后值班人数变成零,即使每个事务都使用了内部稳定的快照。 这叫写偏差,不是对同一行发生丢失更新。分别检查每个医生布尔字段是否合法,并没有表达“总人数至少为一”的全局规则。只保护自己正在修改的那一行,也会漏掉两个决定之间的联系。适用的序列化协议需要覆盖整个不变量,而不只是其中某个局部字段。 具体数据库实现也很重要。PostgreSQL 把读未提交映射为读已提交;它的可重复读会阻止幻读,尽管 SQL 标准允许这一等级存在幻读。这些特性仍不会消除写偏差。因此,应根据引擎文档解释隔离名称,不能假定所有数据库同名级别的实现完全一样,更不能把某一种异常被排除当成所有并发问题都解决。 坏掉的样子 把可串行化理解为数据库实际每次只执行一个事务,会误导性能判断。PostgreSQL 仍并发运行事务并跟踪依赖,某个事务可能在执行途中或提交时收到 40001。应用需要用新的读取重做完整决定,不能只重新提交,也不能继续使用被拒绝快照中计算出来的值。否则虽然换了一次事务,决策前提仍然是旧的。 可串行化也不能修复一个独自运行时就违反业务规则的事务。所有相关写入路径都必须遵守既定契约;如果另外一条不受限制的更新允许最后一名医生离岗,不变量仍会被破坏。更高隔离提供并发保证,不会自动实现尚未编写的业务逻辑。实验中的条件判断与隔离级别必须一起成立。 快速验证 在同一个可丢弃的 PostgreSQL 18 数据库中,打开甲、乙两个新的 psql -X -v ON_ERROR_STOP=1 会话,使用相同角色,显式事务外保留正常自动提交,并选用尚未使用的模式名称。严格遵守下面顺序;先让两边读取重叠,才能建立要观察的竞争决策。 在甲中创建初始值班表。 CREATE SCHEMA isolation_demo; CREATE TABLE isolation_demo.doctors (id integer PRIMARY KEY,on_call boolean NOT NULL); INSERT INTO isolation_demo.doctors VALUES (1,true),(2,true); 先在甲中开始事务并读取,再在乙中执行对应操作。两边人数都应为二。 BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT count(*) AS available FROM isolation_demo.doctors WHERE on_call; BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT count(*) AS available FROM isolation_demo.doctors WHERE on_call; 在甲中更新第一名医生,再在乙中更新第二名。每条更新也会在自己的快照中检查人数规则。 UPDATE isolation_demo.doctors SET on_call=false WHERE id=1 AND (SELECT count(*) FROM isolation_demo.doctors WHERE on_call)>1 RETURNING id; UPDATE isolation_demo.doctors SET on_call=false WHERE id=2 AND (SELECT count(*) FROM isolation_demo.doctors WHERE on_call)>1 RETURNING id; 先提交甲,再提交乙。两边结束后,在甲中查看已经失效的不变量并重置值班表。 COMMIT; \set ON_ERROR_STOP off COMMIT; \echo commit_state :SQLSTATE \set ON_ERROR_STOP on SELECT count(*) AS available FROM isolation_demo.doctors WHERE on_call; UPDATE isolation_demo.doctors SET on_call=true; 使用可串行化重复这组重叠决策:开始甲、开始乙、更新甲、更新乙、提交甲,最后尝试提交乙。 BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT count(*) AS available FROM isolation_demo.doctors WHERE on_call; BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT count(*) AS available FROM isolation_demo.doctors WHERE on_call; UPDATE isolation_demo.doctors SET on_call=false WHERE id=1 AND (SELECT count(*) FROM isolation_demo.doctors WHERE on_call)>1 RETURNING id; UPDATE isolation_demo.doctors SET on_call=false WHERE id=2 AND (SELECT count(*) FROM isolation_demo.doctors WHERE on_call)>1 RETURNING id; COMMIT; \set ON_ERROR_STOP off COMMIT; \echo commit_state :SQLSTATE \set ON_ERROR_STOP on 在甲中检查人数,在乙中以新事务重做完整决定,最后在甲中核对并清理。 SELECT count(*) AS available FROM isolation_demo.doctors WHERE on_call; BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT count(*) AS available FROM isolation_demo.doctors WHERE on_call; UPDATE isolation_demo.doctors SET on_call=false WHERE id=2 AND (SELECT count(*) FROM isolation_demo.doctors WHERE on_call)>1 RETURNING id; COMMIT; SELECT count(*) AS available FROM isolation_demo.doctors WHERE on_call; DROP SCHEMA isolation_demo CASCADE; PostgreSQL 18.6 实测中,可重复读的两个提交都成功,可用人数变成零。在受控的可串行化顺序下,甲提交成功,乙提交返回 40001,仍有一名医生留岗。代码只在需要观察的提交周围关闭遇错停止,让 psql 能打印 SQLSTATE;如果出现其他错误,不能把它当成预期的序列化拒绝。 ...

八月 23, 2026 · 2 分钟 · XB Field Notes
一个事务读旧快照,另一个事务并行写出新版本。

MVCC 根据可见性规则选择行版本

多版本并发控制让 PostgreSQL 保留不同的行版本,再判断查询可以看到哪些版本。普通读者可以继续读取较早已提交的版本,同时另一个事务写出新版本。这减少了读写之间的冲突,但不表示每个事务都永久拥有一幅固定画面。快照寿命取决于隔离级别,而且事务能看到自己此前的修改,即使其他事务尚看不到。把这些边界说清楚,才能理解读取结果为何既稳定又可能变化。 可见性跟随快照 在读已提交级别,普通读取通常每条语句取得一个新快照。PostgreSQL 的可重复读则复用事务第一条非事务控制语句建立的快照。仅仅发送 BEGIN,还没有建立实际读取使用的边界。因此,一个较早开始、较晚才第一次读取的事务,可能看到这两个时刻之间已经完成的提交。“事务启动时间”与“读取快照已经建立”不能随意混用。 更新会创建一个新行版本。可见性规则决定旧快照是否仍读取之前的版本,而新快照是否看到更新。元组物理上仍然存在,并不足以说明当前查询能看到它;SQL 也不是直接返回数据页上最新写下的字节。只有结合事务状态与快照,才能判断哪一版记录属于这次读取的结果。 事务自己的修改,是简单“历史照片”比喻的重要例外。即使快照早已建立,事务仍然可以读到之后由自己插入的记录。它看到的数据,可能同时包含其他事务的较早状态与自己的新工作。这种行为让事务能够实际完成读写操作,但不能把它误解为外部世界某一个已提交时刻的完整照片。理解这个例外,也能避免错误地认为自己的更新必须提交以后才可读。 坏掉的样子 普通读取取得的常规表锁不会阻塞正常行更新,但锁并没有消失。两个写者仍可能争用同一行;SELECT FOR UPDATE 会明确请求行锁;ACCESS EXCLUSIVE 表锁则与普通读者冲突。因此,MVCC 不承诺所有带有 SELECT 的命令都永远不等待。是否阻塞还需要看语句请求的锁以及当前其他操作。 旧快照还可能让读者需要的旧行版本继续保留。只要某个相关快照仍可能看到该版本,清理就不能直接移除它。空间压力取决于更新量、保留版本和清理进度,不只是打开了多少个事务。再启动一次清理,并不会让仍然活跃的快照忘掉自己的可见性要求;需要沿保留原因调查,而不是只盯住维护命令有没有运行。 快速验证 在同一个可丢弃的 PostgreSQL 18 数据库中,打开甲、乙两个新的 psql -X -v ON_ERROR_STOP=1 会话,使用相同角色,显式事务之外保持正常自动提交。模式名称必须尚未被使用。按下面顺序执行,让乙在甲的读事务仍然打开时完成提交。 在乙中建立一条已经提交的原始记录。 CREATE SCHEMA mvcc_visibility_demo; CREATE TABLE mvcc_visibility_demo.items (id integer PRIMARY KEY,note text); INSERT INTO mvcc_visibility_demo.items VALUES (1,'old'); 在甲中通过读取建立可重复读快照。 BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id; 在乙中更新原记录、插入第二条记录,并查看已经提交的结果。 UPDATE mvcc_visibility_demo.items SET note='new' WHERE id=1; INSERT INTO mvcc_visibility_demo.items VALUES (2,'other'); SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id; 在甲中重读,插入属于自己的第三条记录,再读一次并提交。 SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id; INSERT INTO mvcc_visibility_demo.items VALUES (3,'mine'); SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id; COMMIT; 甲提交后使用新语句再次读取,随后在乙中清理。 SELECT id,note FROM mvcc_visibility_demo.items ORDER BY id; DROP SCHEMA mvcc_visibility_demo CASCADE; PostgreSQL 18.6 实测中,甲第一次读取返回 (1,old)。乙不需要等待这个普通读者结束,就完成了更新和插入,并看到 (1,new) 与 (2,other)。甲再次读取时仍然只有 (1,old)。当甲插入自己的记录后,它能看到 (1,old) 和 (3,mine),但乙插入的第二行仍不属于甲的快照。这同时展示了他人提交的隔离与自身修改的可见性。 ...

八月 8, 2026 · 1 分钟 · XB Field Notes