吾正营构之账簿服务,乃Java 21 / Spring Boot 3.5 / PostgreSQL之双录电子钱包账簿也。栖于Render之上早岁吾撰一压力测试,发五十次传输于其上。同一账户 顿觉书卷永无朽坏之理。其色转赤——赤色之变,乃吾筑此间所获至要之识。
是文遍述其理:何以钱簿恒守平衡? 缓存之由。固无谓,缓存所召之读改写竞态,吾如何察之,其修(乐观锁+有界重试),其基准(证乐观锁胜悲观锁),及幂等须与重试相合,使网络小歇不致重费。
设备:一账本,及其设缓存之由
真源乃一复式、仅增之账册 表 (ADR-0005)。每项金钱之运作,必书至少一平衡之 DEBIT/CREDIT 对,其 Σ DEBIT == Σ CREDIT,而 ledger_entries 乃唯增之——无 UPDATE,亦无 DELETE。谬误既生,则书一 以正之。条目,非由修订历史而成。此乃 Stripe、Modern Treasury、Formance皆所采用之模,亦即赋予汝可信赖之审计轨迹者也。
然"X之账户余额何在?"非也。SUM于账户所历之每一项,吾恒存之。缓存:accounts.balance乃成实物也Σ此账目之记录,已更新于此同时交易盖条目本身(ADR-0006)即真也。条目为真,余额乃派生之读缓存,恒为O(1)。
此缓存正为竞态而生。
竞渡
二请同时扣同一账户:
R1: read balance $500 (enough) R2: read balance $500 (enough)
R1: commit −$300 → $200 R2: commit −$400 → −$200 ← overdraft / lost update
二者皆读$500,皆以为足,复书己见,欲新定其衡。一默书,遂覆他书:此乃失序之更,衡不复与簿记相应矣。
所陷者,乃以为数据库自能禁之。非也。PostgreSQL之常态隔离,READ COMMITTED,惟保尔不读未提交之数据——然于两事务各读后写同列而并行者,无能为也。读改写之竞,直穿而过之。
察之:压力之试
此乃现此谬之试。先注一户,再发之。N = 50与之同时迁出,而后稽核账册:
AtomicInteger successes = new AtomicInteger();
CountDownLatch start = new CountDownLatch(1);
ExecutorService pool = Executors.newFixedThreadPool(16);
for (int i = 0; i < N; i++) {
pool.submit(() -> {
start.await(); // line them all up...
int code = post("/transfers", from, to, AMOUNT); // ...then fire at once
if (code == 201) successes.incrementAndGet();
});
}
start.countDown();
// after all complete:
assertThat(balanceCache(from)).isEqualTo(ledgerBalance(from)); // cache == Σ entries
assertThat(balanceCache(from)).isGreaterThanOrEqualTo(0); // no overdraft
assertThat(balanceCache(to)).isEqualTo((long) ok * AMOUNT); // exact accounting
此断言有意 与时序无关 — 适用于 任何成败之分,盖因彼较缓存于账实,而非较之固定预期之数。此乃使试为稳定之回归守卫,而非易变者也。
吾以实验证之,其虫实然:与@Version柱已去八成有五之缓存更新俱失 诸此断言赤色矣——缓存之余额远离诸条目之总和。缓存与真谛相悖,此乃货币体系之全局所在。
整之策,其一:乐观锁
accounts 既存 version BIGINT 之列,盖因账户乃聚合/锁之界域(ADR-0010)。映射为 JPA @Version,则每写余额皆为比较并置:
UPDATE accounts SET balance = ?, version = version + 1
WHERE id = ? AND version = ?
有二同時寫者,皆載版本7。先成者立之為8。後者之UPDATE ... WHERE version = 7,今與零行相合,而Hibernate於成時發OptimisticLockingFailureException。失更新今不可得:非復默然覆壞,敗者為 言于 ,曰其失矣。
其要义在:此乃 之察,非阻 也。无读者尝候锁。于账簿——其收支之数,读远胜于写——此甚要也。
其补,其二:限重试
独察不足。以@Version 既置而不再试,则压力测试虽止于数据之毁,然巨量之传输今复败于冲突——虽正,然体验实劣。故负者当再试之。
重试之助,居外 @Transactional,而此置,即其要义所在:
public <T> T execute(Supplier<T> operation) {
for (int attempt = 1; ; attempt++) {
try {
return operation.get(); // a FRESH transaction each attempt
} catch (OptimisticLockingFailureException e) {
if (attempt >= maxAttempts) throw new ConcurrencyConflictException();
sleep(backoffWithFullJitter(attempt)); // 25–200 ms, capped
}
}
}
每试皆为全新交易,重载其现行版 — 失败交易内重试,徒劳于陈旧之版。默认:五试,指数退避,配全抖动(免雷群复撞),竭尽则净409 Conflict — 终不复500.
有微理焉,使此于中度负荷下可证其必终:第k位提交者,唯能败于异时之早先提交者,故其所需至多k之尝试。四者并行,五次之限,皆可必然成功——无不可靠之测试。真热账户(并行者多于尝试之限)现于409,此乃诚实之缓冲,非隐秘之腐败.
乐观与悲观:权衡之择
显见之替代,乃悲观锁定——SELECT ... FOR UPDATE 莫触行而先锁之,故作者二但待。不重试,思之易明。然何故尚乐观?
吾不欲以感气争之,乃作一测试 (TransferConcurrencyBenchmark),使二策皆行 同此移转之理,五十并作之作者,对一实 PostgreSQL:
| 境况 | 乐观重试 | 悲观FOR UPDATE
|
|---|---|---|
| 争端少五十字之离析户牖 | 三十四毫秒· 各半可也 ·零重试徒劳 | 三十一毫秒 · 五十比五十可 |
| 高争之局五十次转移→一热争) | 毫秒七三一 · 五十比五十無誤 · 毫秒一百八十五重試浪費 | 毫秒三百五十八 · · 五十比五十無誤 |
讀數之義:
- 爭奪甚微乃常態,然平分秋色(毫秒三百四十四對三百一十一)—然樂觀之法浪費零重試之次,至關要者,未嘗阻滯讀取之途此乃重读账簿之决断也。
- 独处炎炎之席,悲观者速约倍之。(358與731毫秒對比)且不浪費,而樂觀者則在碰撞與回退上多耗185次嘗試(≈4.7倍工作量)。然悲觀者之勝,正在此處。序列化并阻塞读取— 事我所欲避者 — 而实非也解热户,但仅作排队处理。
故其判语为乐观+重试,而基准之价值非“乐观速”,(实则争用时非然)——乃在185虚耗之重试,可量真热户之阈限,此真热户(思:每充值扣款共享)SYSTEM_FUNDING此行需真晋升:异步排队或子账户分片,非将全系统转为悲观锁.
具备幂等性的组合
另有途径致重试实为使事更甚若不慎之。客之连接于服务器既决,必重试其全HTTP请求——今汝之患,乃贻误传贻二次。冲突重试与网络微颤重试,实为异事,其一之方,不可损其二也。
故二金端皆需一Idempotency-Key首部(ADR-0012,Stripe之式)。使其得兼并行无碍者,其法若何。主张首:
INSERT INTO idempotency_keys (key, status) VALUES (?, 'PENDING')
ON CONFLICT (key) DO NOTHING; -- committed immediately, before business logic
彼原子插入者,乃序列之枢。谁主其争,则操其事;若并时之请,同其键者,睹既成之PENDING行,遂得409 (空中) 而非重行。既成之钥复演所储应答;复用之钥遇 异体之 ,则得 422 (此乃客户契约违犯,刻意与 409 冲突码相别)。
此故能与前者之重试清整相合:乐观锁之重试位 后。 密钥已据为己有。凡内里尝试,皆在一已据之密钥下进行,故于客全不可见,亦绝无再发之可能。冲突重试与请求幂等之栈,非争斗,乃相济也。
下一步当求
缓存虽速,然可偏移(此乃弊,偏败也)。故须定时而动。调和之职,每自不変之記復得平準,有差異則警之——不自行正,必待吏員更錄。此記錄唯增無減,故真實可永復。
复有热户之限,显于185次重试,此乃次级之真障也:若一列确有争用,则当以异步发布或分片该户,视重试之率,为知已越雷池之信。
要旨所在:于财制之中,缓存与账簿相悖。者,事之败也,而@Version之数据库,不能阻君成之。__JHSNS_SEG_1b425c00_170__之比较并置,使失更不可能;有界重试,加抖动,则寻常负荷下,其隐不易察。基准测试,可示所费之价,及上限所在;而幂等性,则保重试——自各层——不致成倍消费。
代码: github.com/xidoke/ledger-service — 此 并发之模 文及 ADRs 0005, 0006, 0011, 0012 更深入焉。 现场演示: 账簿服务-bjzr.onrender.com(免费实例——首次请求冷启动约五十秒)。












