Day32-14|13.4.5 事务处理设计:在一致性、性能与可用性之间进行架构权衡

对应教材:《系统架构设计师教程(第2版)》第13章“层次式架构设计理论与实践”13.4“数据访问层设计”中的 13.4.5“事务处理设计”。本文面向软考高级系统架构设计师,重点学习事务边界、ACID、隔离级别、并发控制、分布式事务、最终一致性、补偿和企业应用实践。

一、教材定位与学习目标

事务是企业系统中保证业务正确性的关键机制。转账、下单、扣库存、审批、报销和合同签署都涉及多个数据变化,若其中一部分成功、另一部分失败,系统就可能出现余额不平、库存超卖、重复扣款和状态矛盾。事务处理设计的任务,是确定哪些操作必须作为一个不可分割的整体完成,哪些操作可以异步和最终一致,如何处理并发、故障、超时、重试和恢复。

事务并不等于“把所有代码包在一个数据库事务中”。事务范围过大,会导致锁竞争、连接占用、响应变慢和可用性下降;范围过小,又可能破坏业务不变量。尤其在分层架构和服务化系统中,表现层、应用服务、领域对象、数据访问和外部系统的边界不同,事务必须与业务用例和一致性边界相匹配。

本篇学习目标:

  1. 理解 ACID、事务边界和业务一致性的关系。
  2. 掌握隔离级别、并发异常和锁机制。
  3. 区分本地事务、分布式事务、消息事务、补偿和最终一致性。
  4. 设计跨数据库、跨服务和外部系统操作的可靠处理方案。
  5. 能够结合案例题和论文说明事务选择、失败恢复和指标验证。

二、事务的基本概念

1. 原子性

原子性表示一个事务中的操作要么全部成功,要么全部失败回滚。比如订单创建时,订单头、订单行和待支付状态必须保持一致。原子性只对事务覆盖的资源有效,数据库回滚不能自动撤销已经发送的短信、已经发出的快递或外部支付。

2. 一致性

一致性表示事务前后业务约束得到满足。它不仅是数据库约束,也包括领域不变量和跨系统业务规则。例如订单总金额等于明细汇总、账户余额满足约束、审批通过后不能回到草稿。数据库事务可以保证一部分一致性,应用服务和领域模型需要保证业务一致性。

3. 隔离性

隔离性控制并发事务之间相互影响的程度。隔离越强,读写冲突和锁等待可能越多;隔离越弱,吞吐更高但可能出现脏读、不可重复读和幻读。应根据业务数据的重要性和并发特征选择,而不是所有场景默认最高隔离。

4. 持久性

持久性表示事务提交后结果不会因普通故障丢失。它依赖数据库日志、复制、存储、备份和恢复机制。跨系统业务还要考虑消息是否持久化、事件是否可重放和外部交易是否可查询。

三、事务边界的确定

1. 以业务用例为候选边界

应用服务通常对应一个业务用例,是事务边界的候选位置。比如“提交采购申请”可以在本地事务中完成申请、明细和状态;但它不一定应把审批通知、文件扫描和外部预算系统调用都包进去。边界要保护关键不变量,同时避免包含长时间和不可回滚的副作用。

2. 以聚合一致性为边界

聚合内部需要强一致,外部聚合可通过事件和补偿协作。事务覆盖过多聚合,会增加锁和协调成本;覆盖过少,又可能导致状态变化不完整。应明确哪些规则必须立即满足,哪些可以在限定时间内最终达到一致。

3. 以资源和技术边界为约束

单数据库事务容易实现;跨数据库、跨消息系统、跨服务和跨供应商的事务需要额外机制。架构师不能只看代码调用关系,还要识别实际资源、网络、事务管理器和提交协议。

4. 事务边界与请求边界不一定相同

短请求可以包含一个本地事务;长流程、异步任务和人工审批不能一直占用请求和数据库事务。它们应拆成多个短事务,通过持久化状态、事件和补偿推进。

场景 合适的边界思路
单聚合更新 一个短本地事务
多表同库更新 应用服务事务,受控提交
跨服务订单与库存 事件、预留、状态和补偿
银行核心强一致 受控分布式事务或专用协调
文件上传与业务登记 文件状态与业务状态分离
长审批流程 多个短事务加流程实例

四、隔离级别与并发异常

1. 脏读

一个事务读取了另一个尚未提交事务的修改,后者回滚后,前一个事务读到的数据并不存在。需要更高隔离或合适的锁来避免。

2. 不可重复读

同一事务两次读取同一行,期间另一个事务修改并提交,导致两次结果不同。对需要稳定读取的数据,应使用更高隔离、版本或快照机制。

3. 幻读

同一事务按条件查询两次,期间另一个事务插入或删除了符合条件的行,结果集数量发生变化。范围约束和更强隔离可降低风险。

4. 常见隔离级别

隔离级别 主要特征 适用考虑
读未提交 可能读到未提交数据 极少数非关键统计
读已提交 避免脏读 许多普通业务默认选择
可重复读 同一事务读取稳定性更强 需要稳定数据判断的业务
串行化 并发事务近似串行 强一致但吞吐和锁成本高

不同数据库对隔离级别实现和默认值可能不同,不能只背名称。选择时要结合执行计划、锁、MVCC、热点数据和业务错误容忍度。

五、并发控制方法

1. 悲观锁

悲观锁假设冲突可能发生,读取或更新时锁定资源。它适合高冲突、必须避免并发修改的场景,但锁范围和持有时间过大,会导致死锁和吞吐下降。应尽量短事务、按统一顺序加锁并设置超时。

2. 乐观锁

乐观锁使用版本号或更新时间,在提交时检查数据是否被其他事务修改。冲突时拒绝、重试或合并。它适合读多写少和冲突较低的场景,用户需要得到清晰的冲突提示。

3. 原子条件更新

库存扣减可以使用“当前库存大于购买数量时才更新”的原子条件,避免先读后写的竞态。条件更新后要检查影响行数,并将失败转化为业务结果。

4. 串行化和队列

对同一账户、库存或设备按业务键进入队列,可以降低高冲突并发。但队列增加延迟和积压风险,需要设置容量、超时、重试、死信和监控。

5. 分段和分片

把热点资源分散到多个分片或桶中,减少单点锁竞争。分片会增加查询、事务和一致性复杂度,必须有路由和重平衡策略。

六、本地事务与分布式事务

1. 本地事务

本地事务由单个数据库或资源管理器控制,可靠、简单、性能较好。应优先缩小业务到单资源或单聚合内,避免不必要的分布式协调。

2. 两阶段提交

两阶段提交由协调者先询问参与者是否可以提交,再统一提交或回滚。它能够提供较强一致性,但参与者需要长时间保持资源,协调者故障和网络分区可能造成阻塞,性能和可用性成本较高。适合参与者少、强一致要求高且环境可控的场景。

3. 三阶段提交和改进协议

通过增加准备阶段和超时机制降低阻塞风险,但不能彻底消除网络不可靠和参与者状态不一致。考试答题时应说明分布式提交协议的性能、可用性和故障恢复代价,而非只写“使用两阶段提交”。

4. TCC

TCC把业务操作拆为 Try、Confirm和Cancel,要求业务提供预留、确认和取消逻辑。它比数据库分布式事务更贴近业务,可减少长锁,但开发复杂,需要处理空回滚、悬挂、幂等和重试。

5. Saga

Saga将长事务拆成多个本地事务,每一步有对应补偿动作。它适合长流程和微服务,但中间状态可能被观察到,补偿不一定等价于回滚,需要明确业务可接受的最终状态。

6. 可靠消息与最终一致

生产者先可靠保存业务状态和待发送事件,再由消息机制投递;消费者幂等处理,失败后重试或进入死信;通过对账和补偿达到最终一致。关键是明确一致性时间窗口、异常状态和人工处理方式。

方案 一致性强度 性能/可用性 适用场景
本地事务 单资源强一致 较好 单库核心操作
两阶段提交 较强一致 成本较高 受控多资源事务
TCC 业务级一致 设计复杂 可预留、可确认的业务
Saga 最终一致 较高可用 长流程和跨服务业务
可靠消息 最终一致 解耦、削峰 通知、状态传播、异步协作

七、事务传播、嵌套与异常处理

1. 事务传播

被调用业务方法加入现有事务、创建新事务、挂起现有事务或非事务执行,都可能有不同语义。必须明确传播策略,避免一个查询方法意外启动写事务,或内部调用绕过代理导致配置不生效。

2. 嵌套事务

嵌套事务的保存点可以回滚局部操作,但不一定等价于真正独立事务。外部资源和消息发送不能靠数据库保存点撤销。使用时要说明数据库和框架的具体支持。

3. 异常回滚

哪些异常触发回滚、哪些异常允许提交部分结果,必须在业务上定义。吞掉异常后继续提交是常见错误;重试前要判断事务是否已经提交,避免重复副作用。

4. 超时和取消

事务超时后,客户端可能已断开,但数据库或外部系统仍在执行。应用应通过状态查询、幂等和补偿处理未知结果,不能把网络超时直接当成业务失败。

八、案例分析:电商下单与库存扣减

某电商平台在下单接口中开启一个数据库事务,依次调用订单库、库存库、支付平台和短信服务。高峰期出现事务等待、库存超卖、重复支付和短信重复发送。开发团队希望把所有操作改成两阶段提交。

1. 现状分析

订单和库存是不同资源,支付和短信更是外部副作用,无法简单通过本地事务回滚;接口同步等待所有操作,事务时间长;库存先查后扣存在竞态;网络超时后没有状态查询和幂等;两阶段提交会增加参与者阻塞和高峰期资源占用。

2. 目标方案

订单创建在订单库本地事务中完成,产生待支付和待库存状态;库存服务通过幂等消息执行原子预留;支付服务使用支付单号进行幂等扣款;订单根据库存和支付事件推进状态;短信在订单最终状态确定后异步发送。失败时使用取消库存、退款和人工补偿,所有操作支持查询、重试、死信和对账。

3. 一致性策略

订单与订单行强一致,库存预留在限定时间内最终一致,支付结果以支付平台查询为准,短信允许延迟但不能重复。通过订单状态机区分待库存、待支付、已支付、待发货、取消和人工处理。

4. 并发设计

库存扣减采用条件更新或按商品分片队列,检查影响行数;订单使用版本号防止并发取消和支付;重复事件以业务单号和事件ID去重;对账任务核对订单、库存和支付状态。

5. 指标

使用订单成功率、库存差错率、重复支付数、消息延迟、补偿完成率、事务平均时长、锁等待、支付未知状态数和故障恢复时间评价改造效果。

九、综合知识练习与详细答案

练习一:单项选择

事务边界最合理的确定依据是( )。

A. 页面数量 B. 数据库表数量 C. 业务不变量和一致性边界 D. 方法代码行数

答案:C。事务应保护业务不变量,并结合资源和质量属性确定,不能机械按页面、表或代码长度划分。

练习二:多项选择

跨服务事务设计可能采用( )。

A. TCC B. Saga C. 可靠消息与补偿 D. 两阶段提交 E. 把网络调用无限放进本地事务

答案:A、B、C、D。无限扩大本地事务会降低性能和可用性,也无法真正回滚外部副作用。

练习三:案例题

某支付接口调用超时,系统认为扣款失败并允许用户再次支付,后来发现第一次支付已经成功,造成重复扣款。请分析原因并给出改进方案。

参考答案:网络超时只代表调用方没有及时收到结果,不代表外部支付一定失败;系统没有区分成功、失败和未知状态,也没有支付业务幂等和查询机制。改进时应为支付请求生成稳定支付单号和幂等键,调用前后保存状态;超时进入未知状态,先通过支付单号查询外部最终结果,再决定重试或人工处理;支付回调和主动查询都按交易号去重,并校验金额、订单和签名;订单状态机禁止重复支付,重复请求返回已有结果;超过重试阈值进入人工队列并通过对账闭环;记录请求ID、交易号、时间、状态和审计信息;对支付、网络、重试和补偿进行故障演练。

十、案例题答题模板

遇到事务、并发、分布式一致性和支付类题目,可以按以下顺序作答:

  1. 明确业务不变量、资源范围、强一致要求和可接受延迟。
  2. 按聚合和业务用例划分本地事务边界,避免长事务和大范围锁。
  3. 分析脏读、不可重复读、幻读、并发覆盖、死锁和热点。
  4. 根据场景选择本地事务、两阶段提交、TCC、Saga、可靠消息或补偿。
  5. 设计幂等、状态机、超时、重试、查询最终状态、死信和对账。
  6. 区分业务拒绝、技术异常和未知结果,设计人工介入和审计。
  7. 用成功率、重复数、锁等待、延迟、补偿和恢复时间验证效果。

十一、论文写作素材

开头素材

“原有电商下单流程将订单、库存、支付和通知全部放在一个同步请求和数据库事务中,导致事务时间长、资源竞争严重,网络超时后还会产生重复支付。为提升系统在高并发场景下的可靠性和可用性,我参与重新设计事务处理架构,按照业务一致性边界拆分本地事务,并结合幂等、可靠消息、状态机和补偿机制实现跨服务最终一致。”

正文素材

可以重点写四点:第一,如何根据业务不变量和聚合边界定义事务,而不是按页面或数据库表机械划分;第二,如何结合隔离级别、乐观锁、悲观锁、条件更新和队列解决并发问题;第三,如何比较两阶段提交、TCC、Saga和可靠消息,说明一致性、性能、可用性和开发成本的权衡;第四,如何处理超时未知状态、重试、幂等、死信、补偿、对账、审计和指标。要写清哪些操作强一致,哪些允许最终一致。

结尾素材

“实践表明,事务设计的本质不是追求所有操作同时提交,而是在业务可接受的范围内选择合适的一致性模型。通过缩小本地事务、明确状态和补偿、建立幂等与对账机制,系统可以在保证核心业务正确性的同时提升吞吐、可用性和故障恢复能力。”

十二、今日复盘

本节核心结论是:事务边界应由业务不变量和资源边界共同决定;单资源优先使用本地事务,跨服务场景根据一致性需求选择分布式事务、可靠消息、状态机和补偿;超时不等于失败,未知状态必须可查询和可恢复。

请回答:

  1. ACID四个特性分别解决什么问题?
  2. 如何根据业务不变量确定事务边界?
  3. 脏读、不可重复读和幻读有什么区别?
  4. 乐观锁、悲观锁、条件更新和队列分别适合什么场景?
  5. 两阶段提交、TCC、Saga和可靠消息如何权衡?
  6. 支付超时后为什么不能直接认为失败?

请以订单、支付、库存或报销为例,画出“本地事务—事件—远程操作—状态机—补偿—对账”的关系,标注幂等键、锁、重试、死信、人工处理和审计位置。完成后,你就掌握了 13.4.5 的核心内容。