Day37-22|18.6.2 数据库的完整性设计:从约束、事务到可恢复的数据可信链

对应教材目录:第 18 章 安全架构设计理论与实践 → 18.6 数据库系统的安全设计 → 18.6.2 数据库的完整性设计

学习目标:掌握数据库完整性的主要类型和实现方法;理解数据库约束、事务、并发控制、业务状态机、审计和恢复之间的关系;能够针对高并发交易、多租户和分布式系统设计可验证的数据完整性方案。

一、数据库完整性不仅是“数据没丢”

数据库完整性是指数据在结构、关系、取值、业务含义和生命周期上保持正确、合法、一致和可追溯。很多初学者把完整性理解成“数据库有备份,数据就不会丢”,但备份主要解决可恢复性;完整性还包括数据是否被错误写入、重复写入、越权修改、并发覆盖、非法关联和错误恢复。

例如,订单表中有一条记录并不代表订单正确。订单金额可能被改成负数,订单所属租户可能被改成其他企业,订单状态可能从“已完成”退回“待支付”,付款记录可能重复插入,库存可能被并发扣成负数。上述问题都需要完整性设计解决。

数据完整性
   |
   +-- 结构完整性:字段、类型、主键、非空
   +-- 关系完整性:外键、引用、租户归属
   +-- 域完整性:范围、格式、枚举、精度
   +-- 事务完整性:原子性、一致性、隔离性、持久性
   +-- 业务完整性:状态、金额、审批、幂等
   +-- 运营完整性:审计、备份、对账、修复

高级系统架构设计师需要把数据库约束和业务规则结合起来:数据库负责守住底线,应用负责表达业务流程,审计和对账负责发现异常,恢复与修复负责把系统带回可信状态。

二、实体完整性

实体完整性保证每一条记录都能被唯一识别,并且关键标识不会为空或重复。关系数据库通常通过主键、唯一约束和非空约束实现。

1. 主键设计

主键应稳定、唯一、不可随意改变。业务字段如手机号、邮箱和身份证号可能发生变更,不宜直接作为主键。分布式系统可以使用全局唯一 ID、雪花算法、UUID 或数据库序列,但要考虑有序性、索引大小、碰撞风险、生成中心可用性和隐私暴露。

2. 唯一约束

唯一约束适合防止业务上不允许重复的对象,例如同一租户内的用户名、订单号和外部交易号。要特别注意唯一性的范围:全局唯一、租户内唯一、项目内唯一和时间窗口内唯一是不同要求,必须把作用域纳入索引设计。

3. 非空和默认值

关键字段应明确是否允许为空。把所有未知状态都写成空值,会使查询、统计和业务判断混乱。建议区分“尚未产生”“明确为空”“未知”“不适用”等状态,必要时用枚举和状态字段表达。

三、参照完整性

参照完整性保证记录之间的关联关系合法。例如订单必须属于存在的用户和租户,订单明细必须引用存在的商品,付款记录必须引用有效订单。

1. 外键约束

单体数据库中可以使用外键约束、级联更新和限制删除保证关系正确。外键能防止孤儿记录,但在高并发、大规模分库分表和跨服务场景中可能增加锁竞争和跨库复杂度。

2. 分布式场景的替代方法

分布式系统中常采用以下组合:

写入前校验存在性
      + 事务消息/可靠事件
      + 服务端状态机
      + 定期对账
      + 异常数据隔离与修复

例如订单服务不直接依赖商品库外键,而是在下单时通过商品服务确认商品有效,随后把商品快照写入订单;即使商品后来下架,历史订单仍保持当时的业务语义。这样做要补充事件失败重试、对账和异常处理,否则“没有外键”就可能演变为关系失控。

3. 删除策略

物理删除可能破坏历史关系和审计证据。核心业务通常采用逻辑删除、状态归档或不可变历史表,并明确哪些数据可以删除、何时删除以及如何处理关联数据。删除操作应受到授权、审批和审计控制。

四、域完整性

域完整性要求字段值符合规定的类型、范围、格式和业务含义。

常见控制包括:

  • 数值范围:数量不能为负,折扣必须在允许区间。
  • 金额精度:使用合适的定点数,禁止用浮点数表达货币。
  • 时间规则:结束时间不能早于开始时间,时区和夏令时要统一。
  • 枚举约束:状态只能取规定值,禁止用任意字符串代替状态机。
  • 格式校验:手机号、证件号和编码格式由服务端校验。
  • 长度限制:防止超长输入、截断和存储溢出。

前端校验主要用于改善交互体验,服务端和数据库约束才是可信边界。即使数据库不便于实现所有复杂规则,也要在应用层集中校验,避免规则散落在多个客户端。

五、事务完整性与 ACID

1. 原子性

一个事务内的操作要么全部成功,要么全部回滚。转账场景中,扣款和入账不能只成功一半。跨服务场景无法简单依赖单库事务,可以采用可靠消息、事务消息、补偿事务和状态机表达中间状态。

2. 一致性

事务执行前后,数据必须满足约束和业务规则。一致性不是要求所有副本永远同时相同,而是要求在定义的时间和边界内满足业务承诺。架构师要明确强一致、读己之写、最终一致和可接受的暂时不一致。

3. 隔离性

隔离性控制并发事务之间的影响。常见隔离级别包括读未提交、读已提交、可重复读和串行化。隔离级别越高,通常并发性能和锁冲突成本越高。需要根据业务选择,而不是默认“越高越安全”。

4. 持久性

提交成功的数据在系统故障后仍应保留。持久性依赖日志、刷盘、复制、备份和灾备。若业务返回“支付成功”后数据尚未可靠持久化,系统恢复时可能出现资金和订单不一致。

六、并发控制

1. 悲观锁

悲观锁假设冲突会发生,在读取或更新时锁定记录。适合竞争激烈、操作时间短且必须严格串行的场景。缺点是可能造成锁等待、死锁和吞吐下降。

2. 乐观锁

乐观锁通过版本号、更新时间或条件字段检测并发冲突。更新时带上旧版本,若影响行数为 0,表示数据已被修改,需要重新读取或提示用户。它适合读多写少和冲突较低的业务。

3. 原子条件更新

库存扣减可以使用带条件的原子更新:

UPDATE stock
SET available = available - :qty,
    version = version + 1
WHERE sku = :sku
  AND available >= :qty
  AND version = :oldVersion;

如果更新影响行数不是 1,系统应判断是库存不足、版本冲突还是商品状态变化,不能无条件重试,否则可能造成超卖或重复扣减。

4. 幂等设计

幂等表示同一业务请求执行一次或多次,最终结果一致。可以使用业务唯一号、幂等表、唯一索引、状态机和结果缓存。幂等键要有作用域、有效期和处理状态,不能只在内存中记录,否则节点重启后可能重复执行。

七、业务规则与状态机

数据库字段约束无法完整表达业务流程。对于订单、支付、审批、退款和工单,应建立明确状态机。

订单草稿 --提交--> 待审核 --通过--> 待支付 --成功--> 已完成
   |                 |                 |
删除                驳回              超时取消

每个状态转换应定义:执行主体、前置状态、输入条件、数据变化、后置事件、幂等规则和审计内容。服务端不应接受客户端任意传入目标状态,而应根据动作和当前状态计算下一状态。

涉及金额时,还要设计金额不可篡改和可追溯机制。订单金额、优惠、税费和实付金额可以通过明细计算,关键结果保存计算版本和来源;付款、退款和冲正应使用不可变流水,而不是直接覆盖原金额。

八、数据库审计与变化捕获

1. 记录什么

不应只记录“SQL 执行成功”,还要记录主体、来源、对象、旧值摘要、新值摘要、业务单号、请求号、时间、策略版本和结果。敏感字段可以只记录摘要或变化类型,避免审计日志成为新的泄露源。

2. 审计在哪里保存

高价值审计日志应与业务库隔离,采用集中采集、只追加、权限分离和不可变存储。数据库管理员不能同时拥有修改业务数据和删除审计日志的权限。

3. 变更数据捕获

CDC、数据库日志和事件表可以把数据变化传递给搜索、缓存、数据仓库和风控系统。CDC 设计要考虑顺序、重复、断点、主键更新、事务边界、敏感字段脱敏和下游失败重试。

九、数据校验、对账与修复

任何复杂系统都可能出现局部故障,因此除了预防,还要设计发现和修复机制。

1. 校验

可以使用记录数、金额汇总、哈希摘要、版本号、外键扫描和业务规则检查发现异常。校验任务应尽量只读,避免影响在线交易。

2. 对账

内部账、渠道账、银行账和库存账之间应周期性对账。对账不只是找差异,还要定义差异分类、重试、人工复核、补偿和关闭流程。

3. 修复

修复操作必须可审批、可回滚、有范围限制和全量审计。禁止直接修改生产数据后只在聊天记录里说明原因。对于不可逆修复,应先备份、演练和验证影响范围。

十、案例分析:高并发库存与订单系统

某电商系统在大促期间面临并发下单、重复支付、订单超卖和消息重复消费问题。可采用以下完整性设计:

  1. 商品、库存和订单使用稳定业务 ID;同一用户同一活动的订单号在作用域内唯一。
  2. 库存扣减采用原子条件更新或库存分片,并明确库存预占、释放和回补状态。
  3. 订单状态采用状态机,禁止客户端直接写入“已支付”和“已完成”。
  4. 支付请求使用幂等键,支付回调验签并检查金额、商户、订单状态和回调号。
  5. 订单库与库存库之间通过可靠事件和补偿流程协作,定时对账发现差异。
  6. 消息消费者使用业务唯一号去重,失败消息进入重试和死信队列。
  7. 数据库使用唯一索引、版本号、事务和审计表防止并发覆盖。
  8. 关键变更保存不可变流水,统计报表从流水计算,避免覆盖原始事实。
  9. 灾备恢复后先进入只读或对账状态,确认账务和库存一致后再恢复交易。

十一、案例题答题模板

遇到数据库完整性问题,可按以下顺序作答:

  1. 先识别数据对象、关系、关键状态和高风险操作。
  2. 用主键、唯一、非空、检查和参照约束守住结构底线。
  3. 用事务、隔离级别、锁、版本号和幂等解决并发问题。
  4. 用状态机和业务规则限制非法操作和越权变更。
  5. 用审计、CDC、对账和监控发现异常。
  6. 用备份、补偿、回滚和人工复核恢复可信状态。
  7. 说明分布式场景下强一致与最终一致的边界、性能代价和故障处理。

十二、课后练习

练习 1

数据库主键、唯一约束、检查约束分别解决什么问题?

参考答案:主键保证记录唯一识别;唯一约束防止指定范围内重复;检查约束限制字段值和组合条件满足规定范围。

练习 2

为什么分布式系统不能简单地把“取消外键”理解为不需要参照完整性?

参考答案:跨库无法直接使用单库外键,但业务关系仍然存在。应通过写入前校验、快照、可靠事件、状态机、对账和修复任务维护关系,否则会出现孤儿数据和业务不一致。

练习 3

设计支付回调的完整性控制。

参考答案要点:验签、时间窗口、回调号去重、订单号和金额校验、商户校验、状态机、事务更新、幂等结果、审计日志和异常对账。

练习 4

论文题:论“高并发业务系统的数据完整性设计”。至少从六个方面展开。

参考答案要点:数据模型约束、事务与隔离、并发控制、幂等、状态机、消息可靠性、审计、对账、备份恢复、分布式一致性和性能权衡。

十三、今日复盘

  • 我能否区分实体、参照、域、事务和业务完整性?
  • 我是否知道数据库约束与应用业务规则各自的边界?
  • 我能否解释悲观锁、乐观锁、原子更新和幂等的适用场景?
  • 我能否画出订单或支付状态机?
  • 我是否考虑了审计、CDC、对账、修复和灾备恢复?
  • 我能否在案例题中写出“约束—并发—业务—审计—恢复”的闭环?

数据库完整性设计的核心结论是:用数据库约束防止明显错误,用事务和并发控制保护同时发生的操作,用状态机保证业务语义,用审计和对账发现异常,再用可回滚、可验证的修复和恢复机制把系统带回可信状态。