19.1 金融系统应用
本节把场景缩小到一个问题:从账户 A 向账户 B 转账时,怎样保证失败后余额不被部分修改?先掌握这个不变量,再讨论数据库、并发和消息投递。示例只演示单进程内存模型,不处理真实资金,也不提供生产支付系统的完整实现。
前置知识:Result、可变借用、整数运算与集合。代码采用 Rust 2021 edition,不依赖第三方 crate。
19.1.1 语言保证与业务保证分别是什么
Rust 的内存安全检查不会自动证明余额守恒、转账幂等或协议正确。引用计数循环等情况仍可能泄漏内存;unsafe 与 FFI 代码也需要维护各自的安全约束。参见 Rust 官方教材的引用循环说明
。
| 要求 | 本节采取的约束 | 语言检查能否代替它 |
|---|---|---|
| 金额单位一致 | 统一用「分」表达非负金额 | 不能,调用者仍需遵守单位 |
| 失败不修改余额 | 先检查全部条件,再提交两个新余额 | 不能自动证明业务逻辑 |
| 不发生整数溢出 | 使用 checked_add | 需要显式处理错误 |
| 同一请求只转账一次 | 成功后记录请求 ID | 不能自动保证业务幂等 |
19.1.2 完整示例:转账前先验证
保存为 ledger.rs,执行 rustc --edition=2021 ledger.rs && ./ledger:
use std::collections::HashSet;
#[derive(Debug, PartialEq)]
enum TransferError {
ZeroAmount,
InsufficientFunds,
Overflow,
DuplicateRequest,
}
struct Ledger {
balances: [u64; 2], // 单位:分,只支持 A -> B
applied: HashSet<String>,
}
impl Ledger {
fn transfer(&mut self, id: &str, amount: u64) -> Result<(), TransferError> {
if self.applied.contains(id) {
return Err(TransferError::DuplicateRequest);
}
if amount == 0 {
return Err(TransferError::ZeroAmount);
}
let from = self.balances[0].checked_sub(amount)
.ok_or(TransferError::InsufficientFunds)?;
let to = self.balances[1].checked_add(amount)
.ok_or(TransferError::Overflow)?;
// 以上任一校验失败,都没有改变余额或请求记录。
self.balances = [from, to];
self.applied.insert(id.to_owned());
Ok(())
}
}
fn main() {
let mut ledger = Ledger {
balances: [1000, 500],
applied: HashSet::new(),
};
assert_eq!(ledger.transfer("order-1", 300), Ok(()));
assert_eq!(ledger.balances, [700, 800]);
assert_eq!(ledger.transfer("order-1", 300), Err(TransferError::DuplicateRequest));
assert_eq!(ledger.transfer("order-2", 900), Err(TransferError::InsufficientFunds));
assert_eq!(ledger.transfer("order-3", 0), Err(TransferError::ZeroAmount));
assert_eq!(ledger.balances, [700, 800]);
assert_eq!(ledger.applied.len(), 1);
let mut full = Ledger {
balances: [10, u64::MAX],
applied: HashSet::new(),
};
assert_eq!(full.transfer("overflow", 1), Err(TransferError::Overflow));
assert_eq!(full.balances, [10, u64::MAX]);
assert!(full.applied.is_empty());
println!("success, duplicate, insufficient, zero, overflow: passed");
}
预期输出:
success, duplicate, insufficient, zero, overflow: passed
这段代码验证业务校验返回错误时的状态保持。它没有承诺内存不足、进程崩溃或持久化失败时仍然原子提交。余额更新与幂等记录只存在内存中;进程重启后不保留。也没有处理相同请求 ID 携带不同金额的请求冲突。
19.1.3 从教学模型走向服务
不能给 Ledger 外面套一个 HTTP 接口就称作支付服务。至少还需要明确以下边界:
- 数据库事务:余额变更与请求记录一起提交;失败一起回滚。
- 并发写入:在数据库中保证余额条件与更新原子执行,并验证并发请求的失败路径。
- 幂等语义:保存请求指纹和执行结果,相同 ID 但不同内容应报冲突;重试应返回原结果。
- 外部消息:数据库提交与消息投递存在不同失败点,需要设计重试、去重和对账。
- 审计记录:记录业务事件、状态变化和关联 ID,不能只打印一个成功字符串。
这些都是需要实现和测试的约束,不是选用某门语言后自然获得的能力。
19.1.4 性能与场景选择
订单撮合、行情处理和批量计算的负载不同。先确定输入规模、吞吐目标与尾延迟要求,再用相同数据和条件比较实现。不把「Rust 接近 C/C++」「毫秒级响应」作为没有测量条件的性能结论,也不从一个教学转账模型推导高频交易能力。
19.1.5 练习与答案线索
- 将请求记录改为
HashMap,保存金额和结果。重放同一请求应返回原结果;改变金额应报冲突。 - 增加反向转账,列出金额守恒、余额不为负、失败不修改状态三个不变量。
- 在提交前人为制造溢出,检查两个余额和幂等记录都保持原值。不能只断言返回了错误。
2026-10-12 修订:删除无来源的行业推广结论,修正内存泄漏与协议正确性的表述,补充完整案例及五类执行路径。示例执行证据另行记录,不将教学模型表述为生产资金系统。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。