并发上下文 A2 B1 与 A1 B2 逐项取最大值,合并为 A2 B2;合并记录上下文,不替载荷选赢家。

向量时钟区分因果先后与并发

向量为每个被跟踪的参与方记录进度。比较这些计数可以区分因果先后与并发,但如何调和并发值,仍然需要由应用决定。 单一时间戳不能证明因果 标量时间戳可以人为排出顺序,但单凭这个顺序不能证明因果关系。在参与者身份稳定、遵循事件时钟规则的前提下,每个进程为本地事件增加自己的分量;收到消息时,先逐项取最大值合并发送方上下文,再增加接收方自己的分量。后一项不能漏掉,否则接收事件可能与接收前的上下文混为一谈。这些规则记录信息怎样流动,不测量实际经过多少秒,也不会为完全没有交换信息的机器证明物理时间先后。 必须区分四种比较结果 比较需要检查全部分量,缺席的参与者按零处理。只有 A 每项都不大于 B,且至少有一项严格小于 B,才能说 A 严格先于 B。反向支配是后于,全部相同是相等;两边都不能支配对方时才是并发。不能看到“并非 A 先于 B”,就直接推导并发,因为 B 先于 A 和两者相等都还是可能结果。 例如 [A:2] 与 [A:2, B:1] 不是相等,前者严格先于后者。某一个槽位相同,不等于全部历史相同;相反,省略的零槽位与显式零槽位可以表示同一上下文。这里比较的是时钟记录的信息,不是业务数据正文,也不是两份数据在现实世界中看起来有多相似。 并发不替应用决定赢家 并发不会选出赢家,也不一定就是业务冲突。独立字段更新可能可以合并,不兼容的整值替换则可能需要保留兄弟版本或交给用户判断。确定性裁决可以有意丢弃一个值,但那是应用选择的政策,不是时钟发现了先后。相等只表示被编码的上下文相同;只有额外的事件身份契约,才能证明两份载荷确实对应同一事件,不能把向量直接当成正文校验和。 坏掉的样子 时钟大小随方案实际跟踪的身份集合增长。Dynamo 的分量标识协调写入的存储节点,并不是每个曾写过该键的客户端。短命参与者不断出现、重启后计数器归零,都需要专门处理;如果旧身份被复用而计数回退,真正的新操作反而可能看起来像旧历史。身份稳定是算法前提,不是无关紧要的实现细节。 裁掉一个分量,会丢失真实祖先关系。祖先 [A:2, C:4] 先于后代 [A:3, B:2, C:5];从后代删去 C 后,剩下的向量却与祖先呈并发关系。缺席按零,适用于未见过某个参与者的语义,无法替主动遗忘的历史补回证据。Dynamo 论文明确提醒截断会使调和关系不精确。点版本向量采用不同因果表示,并不意味着普通向量的任意槽位都能安全删除。 只比较总和、最大值或旁边的墙钟,同样会丢信息。[A:2, B:1] 和 [A:1, B:2] 总和相同,却互不支配。后续写入应带回读取到的上下文,让存储知道它要替代哪些版本。省略上下文可能产生兄弟版本,具体取决于存储与现有版本,不能说每次都会产生。元数据保留和调和必须按照真实协议实现,不能由这几项算术断言替代。 ...

九月 18, 2027 · 2 分钟 · XB Field Notes
两个可重复读事务均读到两名值班医生,并分别让不同医生离岗。两者提交后留岗人数为零,违反至少一人留岗的规则。

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

选择隔离级别,应从操作必须维持的不变量出发。一致快照很有用,但两个事务可能分别作出看似合理的决定,却在一起提交后得到错误结果。PostgreSQL 的可重复读保持事务读取快照稳定,仍允许部分序列化异常。可串行化则增加依赖检测,拒绝无法解释为任何串行执行顺序的并发组合,同时要求应用处理相应重试。关键是知道业务依赖哪项保证,而不是单纯选择一个听起来更强的名称。 可重复读能保持双方快照稳定,却仍破坏跨行规则。受控可串行化顺序拒绝 B;完整重试重新读到一人,不再更新。此图不增加运行验证证据。 稳定视图与正确决策分开判断 假设有两名值班医生,要求至少一名留岗。每名医生只有在看到可用人数大于一时才允许离开。两个可重复读事务可以都看到两人,然后分别把不同医生标记为离岗。它们没有覆盖彼此的同一行,因此都可能提交,最后值班人数变成零,即使每个事务都使用了内部稳定的快照。 这叫写偏差,不是对同一行发生丢失更新。分别检查每个医生布尔字段是否合法,并没有表达“总人数至少为一”的全局规则。只保护自己正在修改的那一行,也会漏掉两个决定之间的联系。适用的序列化协议需要覆盖整个不变量,而不只是其中某个局部字段。 具体数据库实现也很重要。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