Day32-14|13.4.5 事务处理设计:在一致性、性能与可用性之间进行架构权衡
Day32-14|13.4.5 事务处理设计:在一致性、性能与可用性之间进行架构权衡
对应教材:《系统架构设计师教程(第2版)》第13章“层次式架构设计理论与实践”13.4“数据访问层设计”中的 13.4.5“事务处理设计”。本文面向软考高级系统架构设计师,重点学习事务边界、ACID、隔离级别、并发控制、分布式事务、最终一致性、补偿和企业应用实践。
一、教材定位与学习目标
事务是企业系统中保证业务正确性的关键机制。转账、下单、扣库存、审批、报销和合同签署都涉及多个数据变化,若其中一部分成功、另一部分失败,系统就可能出现余额不平、库存超卖、重复扣款和状态矛盾。事务处理设计的任务,是确定哪些操作必须作为一个不可分割的整体完成,哪些操作可以异步和最终一致,如何处理并发、故障、超时、重试和恢复。
事务并不等于“把所有代码包在一个数据库事务中”。事务范围过大,会导致锁竞争、连接占用、响应变慢和可用性下降;范围过小,又可能破坏业务不变量。尤其在分层架构和服务化系统中,表现层、应用服务、领域对象、数据访问和外部系统的边界不同,事务必须与业务用例和一致性边界相匹配。
本篇学习目标:
- 理解 ACID、事务边界和业务一致性的关系。
- 掌握隔离级别、并发异常和锁机制。
- 区分本地事务、分布式事务、消息事务、补偿和最终一致性。
- 设计跨数据库、跨服务和外部系统操作的可靠处理方案。
- 能够结合案例题和论文说明事务选择、失败恢复和指标验证。
二、事务的基本概念
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、交易号、时间、状态和审计信息;对支付、网络、重试和补偿进行故障演练。
十、案例题答题模板
遇到事务、并发、分布式一致性和支付类题目,可以按以下顺序作答:
- 明确业务不变量、资源范围、强一致要求和可接受延迟。
- 按聚合和业务用例划分本地事务边界,避免长事务和大范围锁。
- 分析脏读、不可重复读、幻读、并发覆盖、死锁和热点。
- 根据场景选择本地事务、两阶段提交、TCC、Saga、可靠消息或补偿。
- 设计幂等、状态机、超时、重试、查询最终状态、死信和对账。
- 区分业务拒绝、技术异常和未知结果,设计人工介入和审计。
- 用成功率、重复数、锁等待、延迟、补偿和恢复时间验证效果。
十一、论文写作素材
开头素材
“原有电商下单流程将订单、库存、支付和通知全部放在一个同步请求和数据库事务中,导致事务时间长、资源竞争严重,网络超时后还会产生重复支付。为提升系统在高并发场景下的可靠性和可用性,我参与重新设计事务处理架构,按照业务一致性边界拆分本地事务,并结合幂等、可靠消息、状态机和补偿机制实现跨服务最终一致。”
正文素材
可以重点写四点:第一,如何根据业务不变量和聚合边界定义事务,而不是按页面或数据库表机械划分;第二,如何结合隔离级别、乐观锁、悲观锁、条件更新和队列解决并发问题;第三,如何比较两阶段提交、TCC、Saga和可靠消息,说明一致性、性能、可用性和开发成本的权衡;第四,如何处理超时未知状态、重试、幂等、死信、补偿、对账、审计和指标。要写清哪些操作强一致,哪些允许最终一致。
结尾素材
“实践表明,事务设计的本质不是追求所有操作同时提交,而是在业务可接受的范围内选择合适的一致性模型。通过缩小本地事务、明确状态和补偿、建立幂等与对账机制,系统可以在保证核心业务正确性的同时提升吞吐、可用性和故障恢复能力。”
十二、今日复盘
本节核心结论是:事务边界应由业务不变量和资源边界共同决定;单资源优先使用本地事务,跨服务场景根据一致性需求选择分布式事务、可靠消息、状态机和补偿;超时不等于失败,未知状态必须可查询和可恢复。
请回答:
- ACID四个特性分别解决什么问题?
- 如何根据业务不变量确定事务边界?
- 脏读、不可重复读和幻读有什么区别?
- 乐观锁、悲观锁、条件更新和队列分别适合什么场景?
- 两阶段提交、TCC、Saga和可靠消息如何权衡?
- 支付超时后为什么不能直接认为失败?
请以订单、支付、库存或报销为例,画出“本地事务—事件—远程操作—状态机—补偿—对账”的关系,标注幂等键、锁、重试、死信、人工处理和审计位置。完成后,你就掌握了 13.4.5 的核心内容。