多人同时编辑配置时,后提交者可能基于旧版本无声覆盖新版本。一个可复现的最小方案是为文档增加单调递增的版本号,并要求每次发布携带读取时的版本。
create table config_documents (
id text primary key,
version bigint not null,
body jsonb not null,
updated_at timestamptz not null default now()
);
create table config_revisions (
document_id text not null,
version bigint not null,
body jsonb not null,
created_at timestamptz not null default now(),
primary key (document_id, version)
);
创建文档时同时保存版本 1 的修订。发布接口接收 expected_version 和新配置,用一条语句完成条件更新与留档:
with changed as (
update config_documents
set body = :new_body, version = version + 1, updated_at = now()
where id = :document_id and version = :expected_version
returning id, version, body
)
insert into config_revisions (document_id, version, body)
select id, version, body from changed
returning version;
返回一行表示成功;返回零行表示文档不存在或版本冲突,应用层再查询当前版本以区分两者。冲突后不能自动套用旧客户端提交的内容,而应让调用方重新读取并显式合并。
回滚也不应把版本号减回去。读取目标修订的 body,再以当前版本作为 expected_version 发布一次。例如当前为版本 2,恢复版本 1 的内容后产生版本 3。历史保持单调递增,回滚本身也能被审计。
可复现验收:
- 两个并发请求都基于版本
1提交不同内容,只有一个产生版本2,另一个得到冲突。 - 冲突方读取版本
2、显式合并后再次发布,产生版本3,修订表每个版本恰好一行。 - 从当前版本恢复任一旧修订,版本继续递增,旧历史不被修改。
- 让回滚与普通发布并发执行,仍只有一个操作成功。
这个方案只负责发现冲突,不自动合并。对权限、路由等高风险配置,显式冲突通常更可控。完整保存 JSON 修订实现简单但占用空间,历史增长后可增加压缩或分层归档。若发布还要刷新缓存,应把待处理事件和新修订放进同一数据库事务,再异步处理外部副作用。