跪拜 Guibai
← All articles
Backend · Interview · Java

Eight Idempotency Patterns and a Four-Layer Defense That Killed Duplicate Payment Tickets

By Rain的Java大神之路 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Duplicate charges and orders are a direct revenue leak and trust killer for any payment or e-commerce system. This guide gives a decision framework and battle-tested code to eliminate them, regardless of your stack.

Summary

Duplicate payments and orders happen because users double-click, networks retry, and MQ redelivers. The solution is idempotency: making any repeated request produce identical side effects. This guide frames idempotency as a four-layer defense: frontend grey-out, gateway interception, business state machine or CAS, and a database unique index as the final correctness guarantee. It also provides a selection table and decision flow for eight patterns, from DB unique index and Token to optimistic lock and idempotency table.

The core code includes a Lua script that atomically checks and reserves a Redis key, an @Idempotent annotation that wraps SET NX EX with parameter fingerprinting, and an MQ dedup table that sits in the same transaction as the business. It also covers pitfalls like check-then-act races, clock rollback in Snowflake, and when to release the placeholder key on failure. The author reports that implementing this stack reduced monthly duplicate-charge tickets to zero.

Takeaways
— Idempotency guarantees identical side effects for repeated execution, not that the call happens only once.
— The golden formula is unique identifier + dedup storage + atomic operation.
— Four layers: frontend, gateway, business CAS, and a unique index as the final line.
— Eight patterns: DB unique index, Token, state machine, optimistic lock, pessimistic lock, distributed lock, idempotency table, MQ dedup table.
— Idempotency keys must be business unique identifiers like order numbers, not per-retry UUIDs.
— The MQ dedup record must be in the same local transaction as the business operation.
— Result snapshot replay makes duplicate requests return the first response instead of erroring.
— Only release the Redis placeholder on clearly retryable failures; use CAS and unique index to protect unknown-outcome cases.
Conclusions

The distinction between idempotency, deduplication, and concurrency control is often muddled; separating them clarifies why locks alone can't stop duplicates.

The four-layer defense accepts that no single mechanism is perfect; the unique index is the only absolute guarantee, which is why it sits last.

Result replay is the often-missed part of true idempotency: f(f(x))=f(x) requires the return value to match, not just the side effects.

Releasing the placeholder on retryable failures is a deliberate trade-off between retry safety and user experience, and it only works because DB constraints back it up.

Concepts & terms
Idempotency-Key
An HTTP header used to send a client-generated unique key with a request, letting the server detect and ignore duplicates.
Check-then-act race
A concurrency bug where two requests both check a condition (like 'not exists') before either writes, leading to duplicate side effects.
Optimistic lock
A version field in a WHERE clause ensures only one concurrent update succeeds; affected row count signals retry or conflict.
Watchdog (Redisson)
A background mechanism that renews a distributed lock lease automatically so the lock doesn't expire while a long business operation is still running.
At-least-once delivery
A messaging guarantee that a message may be redelivered more than once, forcing consumers to deduplicate via business keys.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗