
做数据库课程设计时很多同学会选择“从零实现一个迷你数据库”这类项目但在动手之后往往会发现网上的案例要么只讲 SQL 建表要么只给 CRUD 代码很少有资料把“关系模型、存储引擎、SQL 解析、事务与并发控制”这条完整链路讲清楚。受宾州州立等欧美高校数据库系统课程的启发这类项目不仅要求你能跑通功能更要求你理解一个数据库“内核”是如何工作的。这篇文章将从数据库内核的视角出发带着大家完成一条从关系模型到事务的完整学习路径并给出一个可以运行的极简数据库实现MiniDB。无论你是正在准备数据库课程设计的在校学生还是想补数据库底层知识的后端开发都能在文中找到能落地、能复现、能扩展的内容。本文会涉及一部分中英双语术语因为在学习数据库内核时很多经典概念和论文名称都是英文保留英文术语反而更容易查阅资料。下面我们正式开始。1. 为什么应该手写一个数据库内核很多开发者每天都在用 MySQL、Oracle、达梦等数据库但对数据库“内核”的理解往往停留在“它是一个能存数据、能执行 SQL 的软件”。实际上数据库内核是一个典型的系统软件它由多个精巧的模块组成理解这些模块比单纯背 SQL 重要得多。1.1 数据库内核是什么数据库内核Database Kernel指的是数据库管理系统中最核心的部分它负责存储数据、解释 SQL、组织执行计划、控制并发与事务并提供崩溃恢复能力。可以把数据库内核理解为操作系统的“内核”用户通常接触的是外壳比如命令行接口、可视化工具、ORM 框架而真正支撑这些工具运行的是内核中的一组底层机制。一个完整的数据库内核通常包含以下模块存储引擎Storage Engine负责数据在磁盘或内存中的组织方式包括表数据、索引数据、日志数据。解析器Parser把 SQL 文本转换为抽象语法树AST。优化器Optimizer把一个 SQL 表示成多种执行计划并选择代价最低的那一个。执行引擎Executor按照执行计划逐行处理数据完成扫描、连接、聚合、排序等操作。事务管理器Transaction Manager负责事务的开启、提交、回滚以及并发控制通常基于日志和锁实现。日志与恢复模块Logging Recovery记录数据库的变更历史在宕机后恢复到一致性状态。实际的产品级数据库内核远比这复杂比如 MySQL 的 InnoDB、PostgreSQL、Oracle、达梦等都有自己独特的实现但这六大模块是理解数据库内核的最小框架。1.2 数据库内核的宏观架构如果把一次 SQL 查询执行过程画成一条数据流大致是这样客户端 SQL - 解析器SQL - 抽象语法树(AST) - 优化器AST - 逻辑计划 - 物理计划 - 执行引擎物理计划 - 调用存储引擎接口 - 存储引擎磁盘页、索引、日志实际工程中还会插入权限校验、查询缓存、并发控制等环节。对于一门数据库课程设计来说并不需要真正做到分布式或高并发但至少要能打通“SQL 解析—执行—存储—事务”这条链路。这也是为什么我建议你用一门静态语言或脚本语言去实现一个 MiniDB这个过程能帮你把抽象概念全部落到具体代码上。1.3 本文适合谁学完能收获什么本文适合以下几类读者计算机相关专业学生正在准备数据库课程设计或毕业设计。后端开发工程师对数据库底层实现感兴趣想理解事务、锁、日志的真实原理。准备数据库相关面试需要系统梳理关系模型、事务、隔离级别等知识点的同学。学完本文你将能够说清楚关系模型为什么是现代数据库的基础。理解存储引擎中的“数据字典、页面、记录、日志”等概念。写出一个极简 SQL 词法/语法解析器的核心代码。用一个可运行的 Python 示例亲手实现事务的“提交”与“回滚”。理解事务模式有哪些以及从单机事务到分布式事务的演进逻辑。2. 从关系模型开始数据库的“世界观”关系模型Relational Model由 Edgar F. Codd 在 1970 年提出它奠定了现代关系型数据库的理论基础。很多教材一上来就讲 SQL但其实 SQL 只是关系模型的一种具体语言实现。理解关系模型才能理解为什么数据库中到处都是“表”。2.1 关系模型为何能统治数据库在关系模型出现之前数据库主要采用层次模型和网状模型。这两种模型把数据之间的“指针”作为核心设计查询时往往需要沿着指针遍历开发和维护成本很高。关系模型的核心思想是把数据组织成二维表Relation表与表之间通过公共字段外键关联而不是通过物理指针关联。这种设计有几个明显优势数据独立性高应用层不关心数据在磁盘上的物理位置。集合操作自然可以用选择、投影、连接等关系运算处理数据。理论基础扎实关系代数、关系演算提供了数学层面的保证。所以关系模型的“世界观”就是一切数据都是表一切操作都是对表的集合运算。2.2 表、行、列与约束在关系模型中有一些基础术语需要先统一关系Relation对应一张表Table。元组Tuple对应表中的一行Row。属性Attribute对应表中的一列Column。域Domain属性值的取值范围对应字段类型。基数Cardinality表中行的数量。表还需要满足一些约束其中最常遇到的是主键Primary Key唯一标识一行的列或列组合。外键Foreign Key引用其他表主键的列用于维护引用完整性。唯一约束Unique保证列值不重复。非空约束Not Null保证列值不能为 NULL。Check 约束自定义条件表达式。在数据库课程设计中很多人只把表当成“二维数组”来用实际上约束是关系模型完整性的关键。例如订单表引用用户表主键时如果没有外键约束就可能出现“孤儿订单”。2.3 关系运算与 SQL关系模型提供了若干基础运算SQL 是这些运算的具体语法体现关系运算含义SQL 示例选择Select按条件筛选行WHERE投影Projection按列筛选字段SELECT col1, col2连接Join按条件组合多张表JOIN并Union合并两个结果集UNION差Difference取属于一个结果集但不属于另一个的行EXCEPT笛卡尔积Product所有行两两组合CROSS JOIN聚合Aggregation对一组行计算统计值GROUP BYCOUNT/SUM/MAX/MIN/AVG理解关系运算的价值在于当你在写 SQL 时其实是在描述“我要什么样的集合结果”而不是“我要怎么一步步遍历数据”。优化器的作用就是把这句声明式的描述翻译成高效的执行步骤。2.4 数据字典元数据的存储数据库自身也需要存储“关于数据的数据”也就是元数据Metadata。例如一张表有哪些列每列是什么类型哪个字段是主键这些信息统称为数据字典Data Dictionary / Catalog。在我们后续实现的 MiniDB 中数据字典是最先要设计的数据结构。我通常会用一个 JSON 文件保存表名到列定义的映射结构类似{ users: { columns: [ {name: id, type: int}, {name: name, type: string}, {name: age, type: int} ] } }有了数据字典插入数据时才能知道“第 0 列对应 id第 1 列对应 name”查询时也才能把 SQL 里的列名映射到实际存储中的字段。3. 存储引擎数据文件与页面管理如果说关系模型定义了数据库的逻辑结构那么存储引擎就负责把逻辑结构落到磁盘上。这一部分对于很多人来说比较陌生因为平时写业务代码时很少直接接触“页面”“记录”这类底层术语。3.1 存储引擎要解决什么问题存储引擎需要回答几个问题表数据如何组织是顺序追加到文件末尾还是按主键排序还是按哈希分布索引如何组织是 B 树还是 LSM 树还是哈希索引如何利用磁盘页磁盘读写的最小单位通常是页Page数据库也需要按页管理数据。如何保证写入不丢失需要借助日志。常见的存储引擎组织结构包括堆表Heap Table新记录追加到数据文件末尾适合全表扫描。顺序文件Sorted File按某个字段排序存储适合范围查询但插入成本高。B 树索引组织表Index-Organized Table表本身按主键的 B 树结构存储。LSM 树Log-Structured Merge Tree先写内存缓冲再异步合并落盘适合写多读少的场景。3.2 页面、记录与插槽结构数据库一般不会让每条记录单独占用一个文件而是把文件划分成固定大小的页Page页内再存放多条记录。常见的页大小在 4KB 到 32KB 之间。一个简单的堆表页面结构可以这样理解---------------------------------------------------- | 页头页号、空闲空间偏移、记录数量 | ---------------------------------------------------- | 记录1 | 记录2 | ... | 空闲空间 | ---------------------------------------------------- | 插槽数组指向记录偏移量 | ----------------------------------------------------记录明明已经在页里了为什么还要插槽数组Slot Array因为删除或更新记录后物理偏移量可能会变化插槽数组相当于给每条记录一个稳定的“逻辑指针”。这样可以减少移动大量数据。在我们简化版本的 MiniDB 中可以先用list[dict]表示一张表重点先打通整体流程不必过度纠结页结构。3.3 日志先写日志再写数据数据库保证事务持久性的关键在于日志。最简单的日志体系是 Write-Ahead LoggingWAL核心规则是数据页落盘之前必须先把描述这次修改的日志落盘。为什么需要先写日志因为如果系统在数据写了一半时宕机启动时可以通过日志进行重做Redo或撤销Undo。这比直接保证原子写入磁盘要可靠得多也成为主流数据库的一致性与恢复基础。MiniDB 课程设计不一定需要完整实现 WAL但最好能体现“日志先行”思想。比如事务提交时先把事务操作追加到日志文件再真正修改内存表数据。这样即使程序中途崩溃也能根据日志恢复。3.4 基于 JSON 的最小持久化示例这里先给一个最基础的存储层示例。我们使用 Python 标准库中的json模块把数据字典和表数据分别保存到 JSON 文件。虽然性能无法和真实数据库相比但思路清楚适合作为课程设计的第一版。import json import os DATA_DIR minidb_data def load_json(path, default): if os.path.exists(path): with open(path, encodingutf-8) as f: return json.load(f) return default def save_json(path, obj): with open(path, w, encodingutf-8) as f: json.dump(obj, f, ensure_asciiFalse, indent2) def catalog_path(): return os.path.join(DATA_DIR, catalog.json) def table_path(table_name): return os.path.join(DATA_DIR, f{table_name}.json)这段代码做的事情很简单把数据字典和每张表单独拆成文件读写都封装成函数。实际项目中建议加上异常处理、目录自动创建、文件锁等能力这里只展示核心思路。4. SQL 解析与执行引擎让机器理解查询存储层解决的是“数据怎么存”的问题而 SQL 解析与执行引擎解决的是“用户怎么查”的问题。一个数据库支持 SQL本质上就是把文本翻译成一系列可执行操作。4.1 词法分析与语法分析SQL 处理的第一步是词法分析Lexical Analysis把 SQL 字符串拆成一个个 Token。比如下面这条语句SELECT id, name FROM users WHERE age 18;会被拆成这些 TokenSELECT id , name FROM users WHERE age 18 ;词法分析并不理解语句含义它只负责识别单词、数字、运算符、标点符号。接下来是语法分析Syntax Analysis根据语法规则判断这些 Token 是否能组成合法的 SQL 语句并生成抽象语法树AST。使用 Python 写一个极简 Tokenizer 其实不难。下面是一个简单的分词函数用于处理只包含标识符、数字、括号和逗号的 SQLimport re TOKEN_PATTERN re.compile(r\s*(?Ptoken[A-Za-z_][A-Za-z0-9_]*|\d|[(),;!])) def tokenize(sql): tokens [] pos 0 while pos len(sql): m TOKEN_PATTERN.match(sql, pos) if not m: raise SyntaxError(f无法解析的字符: {sql[pos]}) token m.group(token) if token: tokens.append(token) pos m.end() return tokens print(tokenize(SELECT id, name FROM users WHERE age 18;))运行后会输出[SELECT, id, ,, name, FROM, users, WHERE, age, , 18, ;]这个分词器还没有处理字符串字面量比如WHERE name Alice中的单引号内容你需要额外扩展。真实数据库的词法分析器会用类似 Flex 的工具生成但理解原理后手写一个简单的也完全可行。4.2 递归下降解析器语法分析常用的写法之一是递归下降Recursive Descent解析器它的思路是给每一种语法规则写一个解析函数函数之间互相调用。例如 SQL 的顶层规则可以是statement : SELECT select_list FROM table_name [WHERE condition]对应到递归下降解析器可以定义parse_select()、parse_select_list()、parse_condition()等方法。下面是一个极简例子演示如何把SELECT语句解析成结构化对象class SelectStatement: def __init__(self, column_names, table_name, condition): self.column_names column_names self.table_name table_name self.condition condition def __repr__(self): return fSelectStatement({self.column_names}, {self.table_name}, {self.condition}) def parse_select(tokens): index 0 if tokens[index] ! SELECT: raise SyntaxError(只支持 SELECT 语句) index 1 column_names [] while index len(tokens) and tokens[index] ! FROM: if tokens[index] ! ,: column_names.append(tokens[index]) index 1 if index len(tokens) or tokens[index] ! FROM: raise SyntaxError(缺少 FROM 关键字) index 1 table_name tokens[index] index 1 condition None if index len(tokens) and tokens[index] WHERE: index 1 condition { column: tokens[index], op: tokens[index 1], value: tokens[index 2], } index 3 return SelectStatement(column_names, table_name, condition) tokens tokenize(SELECT id, name FROM users WHERE age 18;) parse_select(tokens)这里把条件拆成{column, op, value}三种要素已经足够覆盖数值型简单过滤。真实 SQL 解析器还要处理AND、OR、子查询、函数、连接等复杂度会高很多。4.3 执行模型火山模型SQL 解析后得到 AST再经过优化器生成物理计划后就到了执行器。传统数据库教材中最为经典的执行模型是“火山模型”Volcano Model也叫迭代模型。火山模型的核心思想每个执行算子都是一个Iterator对外暴露next()接口。上一次算子从子算子获取一行经过自己的处理后再向上返回一行。整个查询计划形成一棵“算子树”最上层不断调用next()就像火山喷发一样把数据一行行“喷”出来。例如SELECT id, name FROM users WHERE age 18的执行计划可以抽象为Projection(id, name) - Filter(age 18) - TableScan(users)在实际代码中可以给每个算子定义一个next()。但在 MiniDB 中如果你想先跑通功能最简单的方式是直接用 Python 列表推导式。例如def execute_select(statement, tables): table tables[statement.table_name] rows table if statement.condition is not None: column statement.condition[column] op statement.condition[op] value int(statement.condition[value]) if op : rows [r for r in rows if r[column] value] elif op : rows [r for r in rows if r[column] value] else: raise ValueError(f不支持的操作符: {op}) if * in statement.column_names: return rows return [{col: r[col] for col in statement.column_names} for r in rows]这种写法虽然不够工程化但非常直观适合用来理解执行器的职责。4.4 解析并执行一条 INSERT只支持 SELECT 的数据库还不能写入数据。我们需要继续扩展解析器和执行器支持 INSERT 语句。假设 SQL 格式是INSERT INTO users (id, name, age) VALUES (1, Alice, 20);这句话的 Token 可能长这样[INSERT, INTO, users, (, id, ,, name, ,, age, ), VALUES, (, 1, ,, Alice, ,, 20, ), ;]你可以先写一个parse_insert()把表名、列名、值分别提取出来然后在存储层调用insert()方法。这种“解析—执行”分离的思路能让你后续不断扩展 DELETE、UPDATE 等语法而不会把代码变成一坨。5. 事务ACID 的完整落地关系模型和存储引擎能让数据库“存得住、查得出”但要让多个用户并发安全地读写数据还需要事务Transaction。事务可能是数据库内核中最值得深挖的部分也是面试中问题最多的地方。5.1 事务为什么存在假设一个银行转账场景账户 A 扣 100 元账户 B 加 100 元。如果 A 扣款后系统崩了B 没有到账用户就会投诉。数据库引入事务就是为了解决这类问题把多个操作打包成一个不可分割的执行单元要么全部成功要么全部失败。事务的经典定义是事务是数据库执行逻辑的一个工作单元由一系列操作组成并且具备 ACID 特性。5.2 ACID 与事务状态机ACID 是四个特性的缩写原子性Atomicity事务中的操作要么全部生效要么全部不生效。一致性Consistency事务执行前后数据库的完整性约束不被破坏。隔离性Isolation多个事务并发执行时彼此不会产生干扰。持久性Durability事务一旦提交修改结果就不会丢失。在实现层面原子性通常靠 Undo Log 或回滚段实现持久性通常靠 Redo Log / WAL 实现隔离性通常靠锁或 MVCC 实现一致性则是对前面三个特性的综合保障。事务状态机描述了事务从开始到结束的迁移过程Active活动 - Partially Committed部分提交 - Committed提交 - Failed失败 - Aborted中止 - Terminated终止实现事务管理器时通常需要给每个事务分配一个事务 ID并跟踪它当前的状态。5.3 事务模式有哪些在看数据库文档或框架时经常会遇到“事务模式”这个说法。常见的事务模式包括自动提交事务Auto-Commit每条 SQL 自动提交MySQL 默认就是这种模式。显式事务Explicit Transaction由开发者显式执行BEGIN/COMMIT/ROLLBACK。隐式事务Implicit Transaction上一事务提交后自动开启新事务SQL Server 早期支持类似模式。批处理事务把一组操作作为一个批次提交减少提交次数。在编程框架中还有编程式事务Programmatic Transaction和声明式事务Declarative Transaction例如 Spring 的Transactional注解就属于声明式事务。理解这些模式有助于在实际项目中合理选择事务边界。5.4 隔离级别从读未提交到串行化并发事务如果完全不加控制会出现四种经典问题脏读Dirty Read读到另一个事务未提交的数据。不可重复读Non-Repeatable Read同一事务中两次读取同一行结果不同因为中间有其他事务提交了修改。幻读Phantom Read同一事务中两次范围查询结果集不同因为中间有其他事务插入了新行。丢失更新Lost Update两个事务同时修改同一行后提交的覆盖了先提交的。SQL 标准定义了四种隔离级别每一种隔离级别能规避的问题不同隔离级别脏读不可重复读幻读读未提交Read Uncommitted可能可能可能读已提交Read Committed不会可能可能可重复读Repeatable Read不会不会可能串行化Serializable不会不会不会不同数据库对隔离级别的实现差异很大。比如 MySQL InnoDB 的可重复读通过 MVCC 和间隙锁Gap Lock基本避免了幻读而 PostgreSQL 的默认隔离级别是读已提交但也支持串行化快照隔离SSI。在课程设计中不需要把每种隔离级别都实现出来但至少要能在代码里看出“当前事务读取的是哪个快照”。5.5 并发控制锁与两阶段锁数据库实现隔离性最常见的手段是锁Lock。锁有两种基本模式共享锁Shared Lock, S Lock允许多个事务同时读同一资源。排他锁Exclusive Lock, X Lock只允许一个事务写资源其他读写都阻塞。为了让多个事务并发调度后得到的结果与某种串行执行结果等价数据库普遍采用两阶段锁Two-Phase Locking, 2PL事务分为“加锁阶段”和“解锁阶段”一旦进入解锁阶段就不能再加新锁。严格的 2PL 能保证冲突可串行化。不过真实数据库中直接使用 2PL 的并不多因为锁竞争会导致性能下降。更多数据库采用 MVCC多版本并发控制写操作创建新版本读操作读取旧版本从而做到读写互不阻塞。这也是为什么很多数据库在高并发下仍然表现不错的原因。5.6 从单机事务到分布式事务面对微服务和分库分表场景分布式事务成为必须考虑的问题。分布式事务会涉及多个数据库节点或消息中间件无法只靠单一数据库的本地事务解决。分布式事务常见解决方案包括两阶段提交2PC协调者先询问所有参与者能否提交再统一提交或回滚。三阶段提交3PC在 2PC 基础上增加准备阶段减少阻塞时间。TCCTry-Confirm-Cancel业务层补偿方案适合跨服务调用。最终一致性基于本地消息表、MQ 事务消息等方式保证短时间内数据最终一致。Seata 等分布式事务框架把 AT、TCC、Saga 等模式封装成易用的中间件。如果搜索引擎中看到“分布式事务一致性”“Seata 分布式事务原理”等热词实际上都是围绕这一类问题展开的。对于数据库内核初学者建议先掌握单机事务再逐步理解分布式事务的协调与补偿机制。6. 手把手实现一个迷你数据库接下来我们进入实战环节。这一节将实现一个简单的 MiniDB它支持create_table创建表。insert插入记录。select查询记录。begin开启事务。commit提交事务。rollback回滚事务。运行时只需要 Python 3.8 和标准库不需要安装任何第三方依赖。我会给出完整代码并解释每一部分的设计原因。6.1 课程设计中的 MiniDB 目标在设计 MiniDB 时我建议先明确范围不追求性能不追求完整的 SQL 标准重点是展示数据库内核的模块划分和事务回滚原理。MiniDB 需要满足以下目标所有数据持久化到本地文件重启后数据不丢失。数据字典独立保存支持多表。支持事务的回滚体现原子性。支持事务提交体现持久性。使用全局锁实现串行化避免并发问题。至于并发性能、MVCC、崩溃恢复等可以作为扩展方向。6.2 项目结构与数据模型项目的目录结构可以很简单minidb/ ├── minidb.py └── minidb_data/ ├── catalog.json ├── users.json └── ...所有代码都放在一个minidb.py文件中方便课堂演示和理解。minidb_data目录会在第一次运行时自动创建。数据模型方面我们用 JSON 对象表示表和列catalog.json保存数据字典。users.json保存 users 表的全部行每一行是一个 dict。6.3 存储与数据字典实现下面实现 MiniDB 的存储部分。为了演示方便我使用一个类来统一管理数据和日志。import json import os import threading class MiniDB: def __init__(self, data_dirminidb_data): self.data_dir data_dir os.makedirs(data_dir, exist_okTrue) self.lock threading.RLock() self.catalog self._load_json(catalog.json, {}) self.tables {} for table_name in self.catalog: table_path os.path.join(data_dir, f{table_name}.json) if os.path.exists(table_path): self.tables[table_name] self._load_json( f{table_name}.json, [] ) else: self.tables[table_name] [] self._txn_active False self._undo_log [] def _path(self, filename): return os.path.join(self.data_dir, filename) def _load_json(self, filename, default): path self._path(filename) if os.path.exists(path): with open(path, encodingutf-8) as f: return json.load(f) return default def _save_json(self, filename, obj): path self._path(filename) with open(path, w, encodingutf-8) as f: json.dump(obj, f, ensure_asciiFalse, indent2) def create_table(self, table_name, columns): with self.lock: if table_name in self.catalog: raise ValueError(f表 {table_name} 已存在) self.catalog[table_name] {columns: columns} self.tables[table_name] [] self._save_json(catalog.json, self.catalog) self._save_json(f{table_name}.json, self.tables[table_name])这段代码实现了最基础的建表和持久化。每次create_table都会同步更新数据字典和表文件。之所以用threading.RLock()加锁是因为 MiniDB 内部操作要保持原子性防止多线程同时修改文件。6.4 事务与回滚实现接下来实现插入和事务控制。为了演示回滚我采用 Undo Log 的思路每次在事务内执行insert时把“这条记录的主键”记录到 undo log回滚时按照相反顺序删除这些记录。def insert(self, table_name, values): with self.lock: self._check_table(table_name) columns self.catalog[table_name][columns] if len(columns) ! len(values): raise ValueError(f列数量不匹配: 需要 {len(columns)}实际 {len(values)}) row {} for i, col in enumerate(columns): row[col[name]] self._parse_value(values[i], col[type]) row[id] len(self.tables[table_name]) 1 if self._txn_active: self._undo_log.append((insert, table_name, row[id])) self.tables[table_name].append(row) self._save_json(f{table_name}.json, self.tables[table_name]) return row[id] def begin(self): with self.lock: if self._txn_active: raise RuntimeError(当前已有未提交事务) self._txn_active True self._undo_log [] print(事务已开始) def commit(self): with self.lock: if not self._txn_active: raise RuntimeError(没有活跃事务) self._txn_active False self._undo_log [] print(事务已提交) def rollback(self): with self.lock: if not self._txn_active: raise RuntimeError(没有活跃事务) for operation, table_name, row_id in reversed(self._undo_log): if operation insert: self.tables[table_name] [ row for row in self.tables[table_name] if row[id] ! row_id ] self._save_json(f{table_name}.json, self.tables[table_name]) self._txn_active False self._undo_log [] print(事务已回滚) def select(self, table_name): with self.lock: self._check_table(table_name) return list(self.tables[table_name]) def _check_table(self, table_name): if table_name not in self.catalog: raise ValueError(f表 {table_name} 不存在) def _parse_value(self, value, value_type): if value_type int: return int(value) if value_type float: return float(value) return str(value)这里需要注意几个关键点id字段不是由用户传入的而是自动生成这样 undo log 可以通过主键精确删除记录。只有在事务开启时才记录 undo log普通模式下直接提交。回滚时从后向前撤销操作保证执行顺序相反这正是 Undo Log 的基本思想。每次修改都立刻持久化到 JSON 文件虽然性能差但保证了重启后数据仍然存在。6.5 运行测试与预期输出把上面的代码整合到同一个类中后我们在文件末尾添加测试代码if __name__ __main__: db MiniDB() db.create_table(users, [ {name: name, type: string}, {name: age, type: int}, ]) # 测试回滚 db.begin() db.insert(users, [Alice, 20]) db.insert(users, [Bob, 25]) print(回滚前数据:, db.select(users)) db.rollback() print(回滚后数据:, db.select(users)) # 测试提交 db.begin() db.insert(users, [Charlie, 30]) db.commit() print(提交后数据:, db.select(users))预期输出类似事务已开始 回滚前数据: [{name: Alice, age: 20, id: 1}, {name: Bob, age: 25, id: 2}] 事务已回滚 回滚后数据: [] 事务已开始 事务已提交 提交后数据: [{name: Charlie, age: 30, id: 1}]看到这个结果说明事务的原子性已经具备了未提交事务被回滚后数据恢复到事务开始前的状态。6.6 还能怎么扩展这个迷你数据库虽然简单但已经具备了一个数据库内核的雏形。如果数据库课程设计需要进一步扩展我建议按以下顺序迭代支持删除和更新并设计对应的 undo log 操作。支持带条件的WHERE查询。实现简单的 B 树索引优化查询。引入 Redo Log模拟宕机后的恢复。实现两种隔离级别比如读未提交和读已提交。用 Flask/FastAPI 写一个 HTTP 接口让 MiniDB 可以被外部程序调用。只要有耐心这个项目完全可以发展成一个中等规模的课程设计项目。7. 常见问题与排查思路在实现迷你数据库时初学者最容易遇到下面几类问题。这里整理成表格方便你快速排查。问题现象常见原因解决思路建表后插入数据报“表不存在”数据字典与表文件加载顺序不对检查__init__中是否先加载 catalog再加载 tables回滚后发现数据没有恢复undo log 记录的主键定位不到记录打印 undo log 和表中数据确认主键是否一致重复运行程序后数据“消失”工作目录不对文件写到了其他路径在代码中打印os.getcwd()和data_dir的绝对路径多线程同时写入时报错没有加锁或锁粒度不够在修改表和文件的方法上统一使用with self.lock修改了表结构后旧数据无法读取数据字典变更没有做兼容设计增加表结构版本号或在加载时做字段映射插入中文时 JSON 文件乱码写入文件时没有指定ensure_asciiFalse统一使用json.dump(obj, f, ensure_asciiFalse)事务内插入大量数据后回滚很慢回滚需要全表遍历删除记录用row_id建立内存索引或记录行号而非主键如果你在运行过程中遇到其他问题我建议先用最小化场景复现只创建一个空数据库执行一次插入再执行一次回滚逐步增加复杂度。8. 最佳实践与工程建议课程设计项目和生产级数据库之间还有很大距离但我们可以从工程角度优化这个 MiniDB 的代码和设计。8.1 从课程设计到工程实现在完成基础功能后可以从以下几个方面提升代码质量引入面向接口的设计把存储引擎、解析器、执行器拆成不同模块避免把所有逻辑堆在同一个类里。统一异常处理自定义DatabaseError、TableNotExistsError、TransactionError等异常便于上层捕获。增加日志输出用logging模块替代print方便后续排查问题。使用配置文件数据目录、日志级别、页面大小等都放入配置提高灵活性。8.2 事务与并发设计建议事务设计时要先想清楚“哪些操作需要处于同一个事务中”。在业务系统中事务边界过大会导致锁持有时间过长事务边界过小又可能破坏原子性。一个常见经验是只把“不可分割的一组写操作”放进事务查询读取尽量走读副本。在数据库内核层面如果要做并发控制我建议优先理解锁和 MVCC 的差别。课程设计中可以先实现简单的全局锁也就是把所有操作串行化在此基础上再尝试按行加锁最后再研究 MVCC。层层推进会更容易理解。8.3 持久化与恢复建议持久化不能只靠“每次修改后立刻全量写文件”这个做法在数据量变大后非常低效。更合理的做法是先写日志再异步把数据页刷盘。日志文件按大小或时间轮转避免无限增长。定期做 Checkpoint记录哪些页面已经落盘缩短恢复时间。MiniDB 可以把这部分当成进阶任务模拟一次“程序中途退出后通过日志恢复未提交数据”的场景非常能提升对 WAL 的理解。8.4 数据库内核学习的推荐路线如果在完成本文内容后想继续深入学习数据库内核可以参考下面的路线先读完《数据库系统概念》前几章把关系代数、SQL、事务概念打牢。学习一门经典公开课例如卡内基梅隆大学的 15-445 Database Systems。阅读 PostgreSQL 或 SQLite 的源码片段不需要全部看懂重点看存储和事务模块。实现一个支持 B 树索引、MVCC 和 WAL 的 MiniDB。最后再接触分布式数据库组件比如 TiKV、Doris、OceanBase 等开源项目的设计文档。如果你经常在搜索引擎看到“数据库课程设计”“分布式事务一致性”“Seata 分布式事务原理”等关键词说明你正在经历从理论到实战、从单机到分布式的成长过程。建议把每一步的基础打扎实不要急于求成。9. 小结与动手建议本文完整走了一遍数据库内核的主线从关系模型理解数据的二维表结构从存储引擎理解数据的落盘方式从 SQL 解析理解查询的执行过程从事务理解并发场景下的安全性与一致性。最后的 MiniDB 代码虽然只有不到两百行却把数据字典、文件持久化、事务提交和回滚这些核心概念串在了一起。如果你正在准备数据库课程设计我的建议是先把今天这份代码在自己的电脑上跑一遍确认输出符合预期然后把后面提到的扩展点列成计划表每周完成一个功能模块。比如第一周加 DELETE 和 UPDATE第二周加 WHERE 条件第三周加简单索引。这样一来你的课程设计既有理论深度又有完整实现答辩时也能讲得非常清楚。数据库内核的学习没有捷径但也没有想象中那么难。当你亲手实现了一次事务回滚看到数据真的恢复到事务开始前的状态时那些曾经在教科书里显得抽象的概念就会变成你真正掌握的工程能力。