Redis Lock Collision Returns Fake Success, Triggering NullPointerException
A distributed Redis lock collision returns a default success response with null data, causing client-side NullPointerException when unpacking; the article analyzes the root cause, shows the flawed code, and provides fixes for both server and client sides.
1. Scenario Introduction
The business scenario involves two systems pulling the same file type concurrently:
System A : Expected puller, scheduled job calling the interface to save files locally.
System B : Another scheduled job that, due to a developer mistake, also pulls the same file type at the same time window.
Business Service : Provides the pull interface; deployed on two nodes sharing a Redis lock to prevent duplicate pulls.
Downstream : Actual data source (DB/ERP).
Business expectation: only System A should pull. When System B's schedule overlaps, it creates two concurrent requests for the same resource.
2. Log Phenomenon Analysis
Logs from the two sides don't match:
Business Service : Shows request received, downstream called, file generated — appears normal.
System A Client : Occasionally reports get data file has exception : null.
Strangely, HTTP status is not a failure code. The client treats the response as success and attempts to unpack the file list, but hits a NullPointerException .
Key Insight : NullPointerException often has no message, so printing e.getLocalizedMessage() yields the literal string null — looking like "exception has no content", but it's actually an NPE.
3. Root Cause: Concurrent Pull by System A and System B
The sequence:
Same time window
│
├─ System B ──► Business Service ──► Acquires Redis lock
│ │ Calls downstream, builds file
│ │ Returns: success + data ✔
│
└─ System A ──► Business Service ──► Lock still held (collision)
│ Returns: success + result=null
│ Client treats as success → NPE ✖Timeline:
Time ──►
System A: Send request ────────────────────────► Get file
Business Service: [Acquire lock]====Processing====[Release lock]
System B: Send request (same window)
│
▼
Collision → Empty success → Unpack NPELog correlation:
First request (System B or first to acquire lock): Business service logs show downstream call present, data produced.
Second request (System A or later arrival): Business service logs show only "received pull request", no downstream call.
The lock is shared across the cluster and guards "same business type must not pull concurrently" — it does not distinguish between System A and System B. From the lock's perspective: first come gets processed, later ones are blocked — blocking is expected; but returning a fake success after blocking is not .
4. Code Pitfall: Collision Response Looks Like Success
Business service logic (simplified):
JSONMessageResponse result = new JSONMessageResponse();
String lock = redis.get("LOCK-" + bizType);
if (lock == null) {
redis.set("LOCK-" + bizType, "1", ttl);
// Call downstream, build file, setResult(...)
} else {
return result; // Collision: direct return
}Many assume new JSONMessageResponse() = "empty failure object" because no NPE occurs. However, the parent class JSONResponse defaults:
private String code = "success"; // Default is success
private Object result; // Default nullSo on collision, the actual response is: { "code": "success", "result": null } While a true success expects:
{
"code": "success",
"result": { "form": { "a.txt": "..." } }
}Comparison of true success vs collision response:
HTTP : Both 200
code : Both "success" (collision uses default)
result : True success has File Map; collision has null
Failure path did not set code to fail, yet reused the success default. To System A, the response looks "successful", but unpacking explodes — the direct cause of the NPE.
5. NullPointerException Source Code
Client-side (System A) typical code:
JSONObject body = response.getBody();
if (!"success".equals(body.getString("code"))) {
log.error("failed");
// Note: some implementations don't return here
}
FormData data = body.getObject("result", FormData.class);
for (String name : data.getForm().keySet()) { // Explodes here
// Write local file
}Step-by-step:
body.code == "success" → Treated as success, continue
body.result == null → After deserialization, data == null
data.getForm() → NullPointerException
Print getLocalizedMessage() → Log shows only ": null"Summary diagram:
System A (expected) ──► success + form ──► Write disk ✔
System B (shouldn't pull)─► success + null ──► getForm() NPE ✖
▲
└── new default code=successTwo issues combined to create the "success yet NPE" failure mode:
Scheduling issue : System B shouldn't pull at this time, yet ran concurrently with System A.
Contract issue : Collision should explicitly fail, but returned fake success; client lacked payload validation.
Missing either would prevent this failure mode.
6. How to Fix the Code
6.1 Server Side: Fail Explicitly on Failure
if (lock != null) {
return JSONMessageResponse.getFailInstance("lock busy");
// Or "locked" / "processing" — never success + null result
}Remember: new objects with default success must not be used directly as failure returns.
6.2 Client Side: Success Code Isn't Enough, Validate Payload
FormData data = body.getObject("result", FormData.class);
if (data == null || data.getForm() == null) {
// Treat as failure or "no data", don't proceed to loop
return;
}One-Sentence Takeaway
System B shouldn't pull but collided with System A → Hit Redis lock; lock blocked re-entry as designed, yet interface returned fake success , caller unpacked into NPE.
Scheduler: Ensure unique puller per resource, separate time windows.
API Writer: Make failure paths explicitly fail.
Caller: Beyond success code, validate payload.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
