架构之路(五):忘记数据库
架构之路(五):忘记数据库
在软件架构的演进过程中,数据库往往被视为系统的核心。我们习惯于先设计表结构,再写业务逻辑,甚至将业务逻辑嵌入 SQL 存储过程中。然而,这种“数据库优先”的思维模式,常常导致架构僵化、扩展困难。今天,我们来探讨一个反直觉的观点:忘记数据库,即从“数据存储”的束缚中解放出来,将关注点转向数据流动和业务语义。## 为什么需要“忘记数据库”?传统架构中,数据库不仅是存储层,还承担了部分业务逻辑(如触发器、外键约束)。这看似便捷,实则在系统规模增长后,会带来以下问题:1. 耦合度过高:业务逻辑与数据存储方式紧密绑定。例如,使用 MySQL 的 JSON 字段存储复杂结构,当需要迁移到文档型数据库(如 MongoDB)时,重构成本极高。2. 性能瓶颈:关系型数据库的 ACID 特性在分布式场景下难以扩展。过度依赖数据库完成聚合查询、排序等操作,会限制系统的水平扩展能力。3. 语义丢失:数据表是面向存储的,而非面向业务的。例如,“用户下单”这个业务行为,在数据库中只是一堆 INSERT 和 UPDATE 操作,丢失了上下文。“忘记数据库”并非真的抛弃数据,而是将数据库视为一个实现细节,就像操作系统中的文件系统一样——它只是我们持久化数据的一种方式,而不是业务架构的核心。## 核心原则:事件溯源与读写分离要实现“忘记数据库”,我们可以引入两个关键模式:事件溯源(Event Sourcing) 和 CQRS(命令查询职责分离)。- 事件溯源:不存储当前状态,而是存储导致状态变更的“事件”序列。比如,不存储“用户余额为 100 元”,而是存储“用户存入了 50 元”和“用户消费了 20 元”这两个事件。从事件流中,我们可以随时重建任何时刻的状态。- CQRS:将“写操作”(命令)和“读操作”(查询)分离。写操作基于事件溯源,读操作则使用独立的、针对查询优化的存储(如内存缓存、Elasticsearch)。这样做的好处是:写端完全脱离数据库思维,只关心业务行为;读端可以根据查询需求自由选择存储技术。## 实战:用 Python 实现一个忘记数据库的记事本下面我们通过一个简单的记事本应用,展示如何不依赖关系型数据库,仅用事件流来实现核心功能。### 示例 1:事件存储与状态重建python# event_store.py - 基于内存的事件存储(可替换为数据库)from dataclasses import dataclass, fieldfrom typing import List, Dict@dataclassclass NoteCreated: note_id: str title: str content: str@dataclassclass NoteUpdated: note_id: str new_content: strclass EventStore: """事件存储:以流的形式保存事件,支持按聚合ID查询""" def __init__(self): self._events: Dict[str, List] = {} # key: 聚合ID, value: 事件列表 def save_event(self, aggregate_id: str, event): """保存事件到对应的流""" if aggregate_id not in self._events: self._events[aggregate_id] = [] self._events[aggregate_id].append(event) def get_events(self, aggregate_id: str) -> List: """获取某个聚合的所有事件""" return self._events.get(aggregate_id, []) def replay_all(self): """回放所有事件(用于系统启动时重建状态)""" # 实际项目中会按聚合ID分组回放 pass### 示例 2:业务对象从事件重建状态python# note.py - 记事本业务对象,不依赖数据库from typing import List, Optionalfrom event_store import EventStore, NoteCreated, NoteUpdatedclass Note: """记事本聚合根:通过事件流重建状态""" def __init__(self, note_id: str, events: List): self.note_id = note_id self.title = "" self.content = "" self.version = 0 # 从事件流重建当前状态 for event in events: self._apply(event) def _apply(self, event): """应用事件,更新内部状态""" if isinstance(event, NoteCreated): self.title = event.title self.content = event.content elif isinstance(event, NoteUpdated): self.content = event.new_content self.version += 1 def update_content(self, new_content: str, event_store: EventStore): """业务方法:更新内容,并生成事件""" event = NoteUpdated(note_id=self.note_id, new_content=new_content) event_store.save_event(self.note_id, event) # 更新本地状态(也可以依赖事件回放) self._apply(event)def create_note(note_id: str, title: str, content: str, event_store: EventStore) -> Note: """创建新记事本""" event = NoteCreated(note_id=note_id, title=title, content=content) event_store.save_event(note_id, event) # 从事件重建Note对象 return Note(note_id, [event])# 使用示例if __name__ == "__main__": store = EventStore() note = create_note("note-1", "购物清单", "牛奶、面包", store) print(f"初始内容: {note.content}") # 输出: 牛奶、面包 note.update_content("牛奶、面包、鸡蛋", store) # 即使重启系统,也可以从事件流重建 events = store.get_events("note-1") recovered_note = Note("note-1", events) print(f"重建后的内容: {recovered_note.content}") # 输出: 牛奶、面包、鸡蛋## 忘记数据库后的架构变化通过以上代码,我们可以看到架构思维的转变:- 写操作:不再有 UPDATE notes SET content = ? WHERE id = ?,取而代之的是生成一个 NoteUpdated 事件。这保证了所有变更都是不可变且可追溯的。- 读操作:Note 对象的状态完全由事件流重建。这意味着我们可以随时改变读模型的结构(例如新增一个字段),而无需迁移数据库。- 存储无关性:EventStore 可以替换为任何持久化方案——文件、MySQL、Kafka(作为事件总线)。但业务代码 Note 完全不感知底层存储。## 现实世界的考量“忘记数据库”并非银弹,它适用于以下场景:1. 需要审计日志:金融系统、电商订单,每个操作都需要留痕。2. 高并发写入:事件追加写入比更新行锁更高效,适合社交 feed、物联网数据。3. 多版本状态重建:比如游戏回放、时间旅行调试。但也要注意陷阱:- 事件版本兼容:业务逻辑变更时,旧事件可能无法被新代码正确解析。需要引入事件版本号和升级策略。- 查询复杂度:事件溯源不擅长复杂查询,通常配合 CQRS 使用专门的读库(如 Elasticsearch)。- 存储膨胀:事件积累会占用大量空间,需定期快照(Snapshot)来优化重建性能。## 总结“忘记数据库”的本质是从数据存储思维转向事件流思维。传统架构将数据库视为系统基石,而事件溯源与 CQRS 告诉我们:数据库只是持久化“事件”的容器,业务的核心是行为而非状态。通过本文的记事本示例,你看到了如何用事件流代替 UPDATE 语句,如何从零散事件中重建完整对象。当你下次设计系统时,不妨先问自己:“如果连数据库都不存在,我的业务逻辑该如何表达?”——答案很可能就在事件流中。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)