如何结合版本控制正确实现REST PUT操作的幂等性
Great question—this is a common point of confusion when pairing REST's idempotency requirements with optimistic concurrency control (your version-based check). Let’s break this down clearly using your example (/objects/1, version 1, name: "alice") to make it tangible.
First, Clarify the Core Concepts
Let’s align on definitions to avoid misalignment:
- Idempotency: Sending the exact same request multiple times leaves the server in the exact same final state as sending it once. It doesn’t mean every request has to succeed—just that repeated requests don’t cause unintended state changes.
- Version-based Concurrency Control: Ensures updates only apply if the client’s view of the resource (its version) matches the server’s current state. This prevents overwriting concurrent changes.
These two are complementary, not conflicting—here’s how to make them work together.
Step-by-Step Implementation with Your Example
Let’s walk through updating /objects/1 from name "alice" (v1) to "bob":
1. Client Fetches the Current Resource
First, the client gets the latest state via GET:
GET /objects/1 HTTP/1.1 Response: HTTP/1.1 200 OK ETag: "v1" # Use this standard HTTP header to pass the version Content-Type: application/json {"id": 1, "name": "alice", "version": "v1"}
(Note: Using ETag is preferred over custom headers—it’s part of the HTTP spec for optimistic concurrency.)
2. Client Sends the PUT Request with Version Context
When updating, the client includes the version (via If-Match header) to signal "only apply this update if the resource is still at version v1":
PUT /objects/1 HTTP/1.1 If-Match: "v1" Content-Type: application/json {"name": "bob", "version": "v1"}
3. Server Processes the Request
The server’s logic should follow this flow:
- Check if the
If-Matchvalue matches the current resource’s version (v1). - If it matches:
- Update the name to "bob".
- Increment the version to v2.
- Return
200 OKwith the updated resource:{"id": 1, "name": "bob", "version": "v2"}
- If it doesn’t match (e.g., another request already updated to v2):
- Return
409 Conflictwith a message explaining the mismatch (e.g., "Current resource version is v2; your request was based on v1").
- Return
How This Satisfies Both Idempotency and Version Control
Let’s test the idempotency rule by repeating the exact same PUT request (with If-Match: "v1" and body {"name": "bob", "version": "v1"}):
- First request: Succeeds, resource becomes v2, name "bob".
- Subsequent requests: Server detects the version mismatch and returns 409. The resource state remains v2, name "bob"—exactly the same as if we’d sent the request once.
This is fully idempotent! The key insight here is that idempotency doesn’t require repeated requests to succeed—it only requires they don’t change the server’s final state.
Common Pitfalls to Avoid
- Mistake 1: Thinking idempotency means all repeated requests must succeed. This is wrong—rejected requests (409) are fine as long as the state doesn’t change.
- Mistake 2: Omitting the version from the PUT request. Without it, you lose concurrency control, and while the request might still be idempotent (e.g., repeatedly setting name to "bob"), you risk overwriting concurrent changes.
- Mistake 3: Using version numbers that aren’t strictly incrementing. Ensure each successful update creates a unique, unskippable version (e.g., sequential integers, UUIDs, or timestamps) so mismatches are unambiguous.
Final Best Practice
Enforce that all PUT requests include a version check (via If-Match or a custom header like X-Resource-Version). This ensures both:
- Idempotency: Repeated identical requests won’t alter the final state.
- Concurrency Safety: Old versions of updates can’t overwrite newer, valid changes.
内容的提问来源于stack exchange,提问作者Doug

