上一篇为事件与逻辑加上版本契约,旧运行终于能够跨部署恢复。现在可以收束九篇原则:稳定身份标记同一件事,追加事件保存事实,纯决定产生命令,发件箱投递意图,幂等回执约束副作用,租约与预算控制恢复。
一、痛点:组件齐全仍可能边界混乱
团队很容易先选数据库、队列和编排框架,再把业务判断塞进 worker 循环。结果是历史读取、网络调用、状态推进和异常重试相互嵌套,任何一步都无法独立重放。最小内核不是功能最少的玩具,而是职责最少且边界可验证的结构。
二、原理:一个确定性核,两个持久化方向
内核读取事件历史,折叠出状态,再根据新事件产生新状态和命令。向内只追加事件,向外只写命令意图;活动执行器把命令变成回执事件。下面用纯内存数据展示一轮循环,刻意不在决定函数里执行命令。
fromdataclassesimportdataclass,replace@dataclass(frozen=True)classState:phase:str="new"token:str|None=None@dataclass(frozen=True)classMessage:kind:strdata:dictdefdecide(state:State,event:Message)->tuple[State,list[Message]]:ifstate.phase=="new"andevent.kind=="trip_requested":returnreplace(state,phase="locking"),[Message("lock_budget",{"activity_id":"act-1"})]ifstate.phase=="locking"andevent.kind=="budget_locked":returnreplace(state,phase="ready",token=event.data["token"]),[]raiseValueError(f"invalid:{state.phase}+{event.kind}")state=State()history=[]outbox=[]foreventin[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.kindforitemincommands])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 唯一、完成回执引用已有命令。示例验证最核心的守恒关系,任何阶段都不能凭空出现。
fromdataclassesimportdataclass@dataclass(frozen=True)classRecord:seq:intevent:strcommands:tuple[str,...]defvalidate(records:list[Record],completed:set[str])->None:known=set()forexpected,recordinenumerate(records,start=1):ifrecord.seq!=expected:raiseValueError("sequence_gap")forcommandinrecord.commands:ifcommandinknown:raiseValueError("duplicate_command")known.add(command)missing=completed-knownifmissing:raiseValueError(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"})exceptValueErroraserror: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,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。