欢迎光临
我们一直在努力

持久化执行前置课(十):画出最小持久化内核,再开始写执行器

上一篇为事件与逻辑加上版本契约,旧运行终于能够跨部署恢复。现在可以收束九篇原则:稳定身份标记同一件事,追加事件保存事实,纯决定产生命令,发件箱投递意图,幂等回执约束副作用,租约与预算控制恢复。

一、痛点:组件齐全仍可能边界混乱

团队很容易先选数据库、队列和编排框架,再把业务判断塞进 worker 循环。结果是历史读取、网络调用、状态推进和异常重试相互嵌套,任何一步都无法独立重放。最小内核不是功能最少的玩具,而是职责最少且边界可验证的结构。

二、原理:一个确定性核,两个持久化方向

内核读取事件历史,折叠出状态,再根据新事件产生新状态和命令。向内只追加事件,向外只写命令意图;活动执行器把命令变成回执事件。下面用纯内存数据展示一轮循环,刻意不在决定函数里执行命令。

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class State:
phase: str = "new"
token: str | None = None

@dataclass(frozen=True)
class Message:
kind: str
data: dict

def decide(state: State, event: Message) –> tuple[State, list[Message]]:
if state.phase == "new" and event.kind == "trip_requested":
return replace(state, phase="locking"), [
Message("lock_budget", {"activity_id": "act-1"})
]
if state.phase == "locking" and event.kind == "budget_locked":
return replace(state, phase="ready", token=event.data["token"]), []
raise ValueError(f"invalid:{state.phase}+{event.kind}")

state = State()
history = []
outbox = []
for event in [
Message("trip_requested", {}),
Message("budget_locked", {"token": "B-7"}),
]:
state, commands = decide(state, event)
history.append(event)
outbox.extend(commands)
print(event.kind, "=>", state.phase, [item.kind for item in commands])
print("history=", len(history), "outbox=", len(outbox), "token=", state.token)

输出:

trip_requested => locking ['lock_budget']
budget_locked => ready []
history= 2 outbox= 1 token= B-7

三、实现:用不变量先约束存储接口

在选择框架前,先写存储端口的契约测试:序号连续、期望版本冲突、事件与命令同事务、命令 ID 唯一、完成回执引用已有命令。示例验证最核心的守恒关系,任何阶段都不能凭空出现。

from dataclasses import dataclass

@dataclass(frozen=True)
class Record:
seq: int
event: str
commands: tuple[str, ...]

def validate(records: list[Record], completed: set[str]) –> None:
known = set()
for expected, record in enumerate(records, start=1):
if record.seq != expected:
raise ValueError("sequence_gap")
for command in record.commands:
if command in known:
raise ValueError("duplicate_command")
known.add(command)
missing = completed – known
if missing:
raise ValueError(f"orphan_receipt:{sorted(missing)}")

records = [
Record(1, "trip_requested", ("act-1",)),
Record(2, "budget_locked", ("act-2",)),
Record(3, "flight_booked", ()),
]
validate(records, {"act-1", "act-2"})
print("valid records=", len(records))
try:
validate(records, {"act-3"})
except ValueError as error:
print("caught", error)

输出:

valid records= 3
caught orphan_receipt:['act-3']

四、踩坑:一开始就追求分布式规模

单机 SQLite 足以验证事务边界、重放和崩溃语义。过早引入多分区队列会把业务错误藏在基础设施噪声里。另一个坑是把快照当事实源;快照只是缓存,删除后必须能由事件重建。监控指标同样不能代替事件,指标可采样、可丢失,不适合裁决业务状态。

最小内核也要从第一天保存模式版本、逻辑版本、活动 ID 和停止原因,否则以后补字段无法解释旧记录。安全边界应默认拒绝未知事件和未知版本。人工操作通过命令进入同一事件链,而不是提供一个绕过历史的“强制改状态”按钮。

五、验证:先证明纯核,再连接数据库

验收顺序应是状态转移表、历史重放、非法事件、命令稳定性,然后才是数据库事务、队列重复和进程崩溃。每加入一种基础设施,都保留前一层快速测试。最终从空库执行一条旅行历史,关闭进程、重新打开并重放,所得状态和命令摘要必须一致。

至此,090 的证据包不再只是任务终点:它启发了持久化执行从一开始就保存可验证事实。这十篇完成了从证据消费到内核蓝图的桥接。下一篇将真正从零实现 miniflow,先把旅行预订工作流写成无数据库、无网络的纯状态机,再逐步为它接上持久化能力。

参考来源

  • Temporal:核心应用结构
  • Martin Fowler:Functional Core, Imperative Shell
  • SQLite:事务

👍 觉得有用就点个 赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。

🚀 本文属于 《持久化执行前置课》 系列,持续更新,关注不迷路。

📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到 cj2664@qq.com,我免费发你。 如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。

赞(0)
未经允许不得转载:171主机测评 » 持久化执行前置课(十):画出最小持久化内核,再开始写执行器
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址