1.8 数据库的奇妙之旅
从无纸化办公的重复副本走到共享数据库,用查询边界、事务、并发和权限追踪数据为什么可靠。
学习目标
- 能从无纸化办公的本地副本追踪到共享数据库的模式、查询、事务和权限边界
- 能用两个会话的提交历史解释并发访问、原子性问题与可接受的一致性结果
- 能在正常、边界和单一故障样本中定位冗余、不一致、越权、丢失更新和部分提交
1.8 数据库的奇妙之旅
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 1.8 数据库的奇妙之旅。正文、查询、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
数据库的奇妙之处不是“把纸换成了屏幕”,而是把共享事实放进一个可验证的状态机:模式声明形状,约束拒绝非法状态,查询读取一个明确的版本,事务把多步修改绑定成提交或回滚,权限决定谁能看到或改变它。只要把这些边界混成“保存一下”,冗余、不一致、部分写入和越权都会重新出现。
三个会让数据库故事失真的陷阱
七个目录节点到机制证据
无纸化办公
↡让记录从纸面流程进入可检索、可授权、可校验的共享系统;它不自动消除缓存或副本。首先要回答“哪一份是共享事实”。例如订单状态可以由数据库中的一行拥有,报表和客户端缓存只能引用它;每个副本都要有来源、版本和失效策略。没有这个所有权声明,电子文件仍会产生两套互相矛盾的真相。
数据的冗余和不一致
↡同一事实在多个位置重复保存,更新路径不一致时出现不同值的状态。不是“数据多”这么简单,而是同一业务身份映射到多个值。用唯一键、外键、规范化边界或明确的缓存失效策略减少重复;若保留冗余,就把重建规则和校验时间写进合同。验收时同时读取主表和副本,不能只看写入成功。
李氏查询
↡本页把目录中的“李氏查询”作为一个带有谓词、快照、结果和权限边界的查询节点来练习。不是一句神秘的数据库口令。一次可复核查询至少要保存:查询条件、使用的表或索引、看到的版本、结果数量、调用角色和耗时。查询结果只能证明“在该版本和权限下符合谓词”,不能直接承诺随后写入仍然安全。
并发访问
↡多个会话交错读取和写入同一组数据时,由隔离、锁或版本检查共同裁决提交历史。要求观察交错顺序,而不是只看最后一个数字。两个会话都读取余额 100,再各自写回 70 和 80,如果没有锁、版本检查或足够的隔离,最后的 80 可能掩盖一次丢失更新。把读取版本、写入前提和提交顺序写进日志,才能判断历史是否可接受。
原子性问题
↡一组相关写入要么整体成为提交结果,要么在失败时整体撤销,不留下业务上不可解释的中间状态。出现在“扣款成功但流水失败”或“库存减少但订单没有生成”时。原子性只保护事务边界内的操作;外部邮件、文件和远程服务仍需要幂等键、事务外盒子或补偿合同。验收的关键是注入第二步失败,再证明第一步不会独立留在数据库里。
安全
↡以身份认证、角色授权、最小权限、参数化查询和审计记录共同保护数据的可见性与可修改范围。不是“密码正确”就结束。应用服务账号应只能访问需要的表和动作,查询参数不能拼接成任意 SQL,敏感读取要能回溯到角色和请求。用拒绝样本验证越权路径,用审计记录证明允许路径,二者缺一不可。
1.8 数据库的奇妙之旅
把七个节点串起来:无纸化办公提出共享事实;冗余与不一致暴露副本成本;李氏查询固定读取边界;并发访问要求提交历史可解释;原子性问题要求失败不留下半成品;安全限制谁能观察和改变状态。1.8 数据库的奇妙之旅是一条从记录载体走向状态合同的路径,而不是某个产品宣传语。
输入、状态与提交合同
type AccountState = {
accountId: string;
version: number;
balance: number;
ledgerEntries: number;
};
type TransactionAttempt = {
session: "A" | "B";
readVersion: number;
nextBalance: number;
permission: "read-write" | "read-only" | "denied";
outcome: "commit" | "rollback" | "retry";
};
function mayCommit(attempt: TransactionAttempt, state: AccountState) {
return (
attempt.permission === "read-write" &&
attempt.readVersion === state.version &&
attempt.nextBalance >= 0
);
}合同把四件事分开:查询读到哪个版本,谁有权写,写入是否满足约束,以及多步结果如何提交。若版本已经变化,重试或回滚比静默覆盖更诚实;若权限被拒绝,不能靠业务层“通常不会发生”继续执行。
用 SQL 形状表达同一合同:
BEGIN;
SELECT balance, version FROM account WHERE account_id = :id;
UPDATE account
SET balance = :next_balance, version = version + 1
WHERE account_id = :id AND version = :read_version AND balance >= 0;
-- affected_rows = 1 才能 COMMIT;否则 ROLLBACK 或重试
COMMIT;这里的 version 是并发证据,不是装饰字段;affected_rows = 0 表示前提已经失效。真正的部署还要选择与业务目标匹配的隔离、锁和超时策略,并记录它们。
四步复核数据库之旅
1. 画出共享事实和副本
列出纸面记录、应用文件、缓存、共享表和报表,给每个值标注所有者、版本与同步方向。用正常样本确认只有一个权威写入口,再用边界样本观察同步延迟。
Lab
两会话提交实验
只改变一个条件,观察版本检查、事务边界和最后的处理选择。
A 读版本 4,更新成功并提交
A: read v4 → update 1 row → commit; balance 100 → 70
判定
commit:约束满足,历史可重放
当前场景:基线提交;记录读版本、影响行数、角色和提交/回滚事件。
正常、边界与单一故障证据
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 单会话、权限有效、版本未变化 | 约束满足后整体提交 | 查询快照、版本、角色、提交记录 |
| 边界 | 两个会话竞争同一版本或副本延迟 | 一方提交,另一方重试或回滚 | 交错顺序、影响行数、隔离/锁策略 |
| 单一故障 | 事务第二步、权限判断或日志写入失败 | 数据不留半成品,失败可追踪 | 失败点、回滚结果、审计与重放 |
故障诊断:先找状态改变的最早节点
- 先核对身份与权限:确认请求角色、目标表、动作和参数化边界;被拒绝的请求不能进入写入路径。
- 再核对查询版本:记录谓词、快照、返回值和读取时间;如果写入前版本变化,判为冲突而不是覆盖。
- 再核对约束与事务:检查唯一键、非负余额、外键和
affected_rows;第二步失败时确认第一步已经回滚。 - 最后核对副本和审计:比较权威表、缓存、报表与日志的版本,区分复制延迟、缓存过期和真正的数据不一致。
不要把“最终余额看起来正确”当作通过标准。一次丢失更新可能被下一次写入偶然掩盖;一次越权读取也可能没有改变数据。诊断必须同时覆盖允许、拒绝、冲突、回滚和重放样本。
最小数据库证据包与反例
证据包包含请求身份与角色、查询文本或参数摘要、读取版本、事务边界、锁或隔离选择、约束结果、影响行数、提交/回滚结果、副本版本和审计事件。对敏感值只保留必要的脱敏摘要,但不能删除定位冲突所需的身份和版本。
反例一是两个会话都读到 100,先后把余额写成 70 和 80,最终 80 看似合法却丢掉了 A 的扣款;反例二是订单已写入但流水插入失败,数据库留下无法对账的半成品。为每个反例保存交错轨迹,在加入版本检查或事务边界后重放,证明修复改变了最早偏差。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 无纸化办公
把记录放入可检索、可授权和可校验的共享系统,不等于所有副本自动一致。
- 数据的冗余和不一致
同一事实在多个位置重复保存且更新路径不同,导致值或版本分叉。
- 李氏查询
本页用于练习的带谓词、快照、结果和权限边界的查询节点。
- 并发访问
多个会话交错读写,由隔离、锁或版本检查裁决提交历史。
- 原子性问题
相关写入必须整体提交或整体撤销,避免业务上不可解释的中间状态。
- 安全
由身份、最小权限、参数化查询和审计共同限制数据的可见与可改范围。
练习
练习
问题 1: 两个会话都读取账户余额 100。A 写入 70 后提交,B 随后写入 80 也提交。你会保留哪些证据,如何阻止丢失更新?
问题 2: 订单行已经写入,但流水写入失败。为什么这不是“稍后补一条就行”,你先如何修复?
问题 3: 报表账号能够读取薪资表,应用账号还能执行任意查询。哪两类证据说明安全边界未成立?
本页小结
1.8 数据库的奇妙之旅的关键不是把纸张换成文件,而是把共享事实放进模式、约束、查询版本、事务和权限合同。完成标准是能解释无纸化办公为什么仍会有冗余,能用李氏查询固定读取边界,能重放并发访问与原子性问题,并用角色、版本、回滚和审计证据证明安全边界确实成立。