惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Securelist
L
LangChain Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC
月光博客
月光博客
S
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
美团技术团队
S
Security @ Cisco Blogs
人人都是产品经理
人人都是产品经理
L
LINUX DO - 热门话题
S
SegmentFault 最新的问题
D
DataBreaches.Net
C
CXSECURITY Database RSS Feed - CXSecurity.com
Microsoft Security Blog
Microsoft Security Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
N
News | PayPal Newsroom
W
WeLiveSecurity
F
Fortinet All Blogs
The Hacker News
The Hacker News
The Register - Security
The Register - Security
P
Palo Alto Networks Blog
Engineering at Meta
Engineering at Meta
T
The Exploit Database - CXSecurity.com
Vercel News
Vercel News
G
GRAHAM CLULEY
博客园 - 聂微东
P
Privacy International News Feed
P
Privacy & Cybersecurity Law Blog
Hacker News: Ask HN
Hacker News: Ask HN
Webroot Blog
Webroot Blog
G
Google Developers Blog
T
Tailwind CSS Blog
V
Vulnerabilities – Threatpost
L
Lohrmann on Cybersecurity
T
Tenable Blog
Cisco Talos Blog
Cisco Talos Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
Security Affairs
云风的 BLOG
云风的 BLOG
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
B
Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hugging Face - Blog
Hugging Face - Blog
T
Threatpost

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How I Rebuilt a Three-Layer Cache System in Java — Redis, L1, and MongoDB Done Right
Mustafa Bing · 2026-04-26 · via DEV Community

I've been working on Nexus, a backend infrastructure project, and recently hit a point where the data synchronization layer needed a serious rethink. What looked like a working cache system turned out to have a broken hierarchy, silent data loss paths, race conditions, and a latent deadlock waiting to happen.

This post walks through every problem I found in the original code and exactly how I fixed each one. Code comparisons included throughout.


The Architecture: What We're Building

The system manages data across three layers:

┌─────────────────────────────────┐
│        Redis Cache (MASTER)     │  ← single source of truth
└────────────┬──────────┬─────────┘
             │          │
       pull 10s      flush 15s
             │          │
┌────────────▼──┐  ┌────▼──────────────┐
│  L1 Cache     │  │     MongoDB        │
│  (in-memory)  │  │  (persistent DB)   │
└───────────────┘  └────────────────────┘

Enter fullscreen mode Exit fullscreen mode

The rules are simple:

  • Redis is always master. No other layer can override it.
  • L1 is an in-memory mirror of Redis. It reads from Redis, never the other way around.
  • MongoDB is the persistent backup. Redis writes to it — not the other way around.

Three scheduled tasks keep everything in sync:

Task Interval Job
L1 Sync 10s Pull Redis → update L1 if changed
Auto Flush 15s Push dirty keys from Redis → MongoDB
Reconciliation 3 min Compare Redis ↔ MongoDB, Redis wins

Now let's look at what was actually wrong with the original implementation.


Problem 1: The Hierarchy Was Backwards

This was the most fundamental issue. The reconciliation task was supposed to enforce Redis as master — but it was doing the opposite.

The original code

private void startReconciliationTask() {
    redisManager.processTask(() -> {
        idToDataList.forEach((keyTag, model) -> {
            if (dirtyKeys.contains(keyTag)) return;

            NexusApplication.getApplication().getMongoManager()
                .getValue(model.getAddon(), model.getSpecificDbKey())
                .thenAccept(dbJson -> {
                    if (dbJson == null) return;
                    try {
                        String cleanDbJson = model.getAddon().modelInitComp(dbJson);
                        if (!cleanDbJson.equals(model.getValueJson())) {
                            model.setValueJson(cleanDbJson);          // ← overwriting L1 with Mongo
                            redisManager.setData(keyTag, cleanDbJson); // ← overwriting Redis with Mongo!
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
        });
    });
}

Enter fullscreen mode Exit fullscreen mode

When MongoDB had a different value than Redis, it was writing MongoDB's value into Redis. MongoDB was effectively acting as master. The entire priority chain was inverted.

The fix

private void startReconciliationTask() {
    RedisManager rm = NexusApplication.getApplication().getRedisManager();

    rm.processTask(() -> {
        // ...
        // Get the master value from Redis first
        String redisJson = rm.getData(key).orElseGet(model::getValueJson);

        NexusApplication.getApplication().getMongoManager()
            .getValue(model.getAddon(), model.getSpecificDbKey())
            .thenAccept(dbJson -> {
                // ...
                String cleanDbJson = model.getAddon().modelInitComp(dbJson);

                // Redis wins. If Mongo is different, update Mongo — not Redis.
                if (!cleanDbJson.equals(redisJson)) {
                    NexusApplication.getApplication().getMongoManager()
                        .setValue(model.getAddon(), model.getSpecificDbKey(), redisJson);
                    model.setValueJson(redisJson); // L1 follows Redis
                }
            });
    });
}

Enter fullscreen mode Exit fullscreen mode

Redis is fetched first and treated as the ground truth. When there's a mismatch, MongoDB gets corrected — never Redis.


Problem 2: Silent Data Loss on Flush Failure

The auto flush task was removing the dirty flag before confirming the MongoDB write succeeded.

The original code

private void startAutoSyncTask() {
    List<String> keysToSync = new ArrayList<>(dirtyKeys);
    dirtyKeys.removeAll(keysToSync); // ← removed before writing to Mongo

    redisManager.processTask(() -> {
        for (String key : keysToSync) {
            DataModel model = idToDataList.get(key);
            if (model != null) {
                NexusApplication.getApplication().getMongoManager()
                    .setValue(model.getAddon(), model.getSpecificDbKey(), model.getValueJson());
                // if this fails, the key is already gone from dirtyKeys
                // it will never be retried
            }
        }
    });
}

Enter fullscreen mode Exit fullscreen mode

If the MongoDB write threw an exception or the future completed exceptionally, the dirty flag was already removed. The entry would never be retried. Data was silently lost.

The fix

private void startAutoFlushTask() {
    List<String> keysToFlush = new ArrayList<>(dirtyKeys); // snapshot — don't removeAll yet

    rm.processTask(() -> {
        for (String key : keysToFlush) {
            DataModel model = keyToModel.get(key);
            if (model == null) {
                dirtyKeys.remove(key);
                continue;
            }

            String jsonToWrite = rm.getData(key).orElseGet(model::getValueJson);

            try {
                NexusApplication.getApplication().getMongoManager()
                    .setValue(model.getAddon(), model.getSpecificDbKey(), jsonToWrite)
                    .get(); // block until Mongo confirms the write

                dirtyKeys.remove(key); // only remove AFTER confirmed success

            } catch (Exception e) {
                // leave the key dirty — it will be retried on the next flush
                LOGGER.log(Level.SEVERE, "[AutoFlush] Write failed, will retry: " + key, e);
            }
        }
    });
}

Enter fullscreen mode Exit fullscreen mode

Two changes here: the dirty flag snapshot is taken but not immediately cleared, and dirtyKeys.remove(key) only runs after .get() confirms the write succeeded. If anything goes wrong, the key stays dirty and gets retried 15 seconds later.


Problem 3: TOCTOU Race Condition in removeModel()

TOCTOU stands for Time-of-Check to Time-of-Use. The original removeModel() checked if a key existed before removing it — but another thread could delete it between those two operations.

The original code

public void removeModel(String key) {
    if (idToDataList.containsKey(key)) {  // Thread A checks: key exists
        // --- Thread B deletes the key here ---
        dirtyKeys.remove(key);
        idToDataList.remove(key);          // Thread A removes: but key is already gone
        NexusApplication.getApplication().getRedisManager().deleteData(key);
    }
}

Enter fullscreen mode Exit fullscreen mode

In a concurrent environment, this is not safe. The containsKey check and remove call are two separate operations with no atomicity guarantee between them.

The fix

public void removeModel(String key) {
    DataModel removed = keyToModel.remove(key); // atomic: check + remove in one step
    if (removed == null) return; // wasn't there — nothing to do

    idToKey.remove(removed.getId());
    dirtyKeys.remove(key);
    NexusApplication.getApplication().getRedisManager().deleteData(key);
}

Enter fullscreen mode Exit fullscreen mode

ConcurrentHashMap.remove() is atomic. Its return value tells you whether anything was actually removed. One operation, no race.


Problem 4: Deadlock Risk in Reconciliation

The original reconciliation task was dispatching a new processTask from inside an already-running processTask.

The original code

redisManager.processTask(() -> {
    idToDataList.forEach((keyTag, model) -> {
        // ...
        NexusApplication.getApplication().getMongoManager().getValue(...)
            .thenAccept(dbJson -> {
                // ...
                redisManager.processTask(() ->   // ← new task dispatched from inside a running task
                    NexusApplication.getApplication().getMongoManager()
                        .setValue(...));
            });
    });
});

Enter fullscreen mode Exit fullscreen mode

If processTask uses a single-threaded executor (which is common for Redis clients to ensure command ordering), submitting a new task from inside a running task means the inner task can never start — the outer task is blocking the only available thread. That's a deadlock.

The fix

Everything runs in a single processTask context. The MongoDB writes inside thenAccept are plain async calls — no nested processTask.

rm.processTask(() -> {
    for (String key : keys) {
        // ...
        CompletableFuture<?> future = NexusApplication.getApplication()
            .getMongoManager()
            .getValue(...)
            .thenAccept(dbJson -> {
                // ...
                // direct Mongo write — no nested processTask
                NexusApplication.getApplication().getMongoManager()
                    .setValue(...);
            });

        batch.add(future);
        if (batch.size() >= RECONCILE_BATCH_SIZE) {
            waitForBatch(batch);
            batch.clear();
        }
    }
});

Enter fullscreen mode Exit fullscreen mode

All reconciliation work happens inside one task. No task dispatches another task.


Problem 5: MongoDB Request Storm on Large Datasets

The original reconciliation fired a MongoDB query for every single entry simultaneously — no throttling, no batching.

On 1000+ entries, that means 1000+ concurrent MongoDB reads, followed immediately by potentially 1000+ writes. This can exhaust connection pools, spike latency, and cause cascading failures under load.

The fix: batch processing

private static final int RECONCILE_BATCH_SIZE = 50;

// inside startReconciliationTask:
List<CompletableFuture<?>> batch = new ArrayList<>(RECONCILE_BATCH_SIZE);

for (String key : keys) {
    if (dirtyKeys.contains(key)) continue;
    // ...

    CompletableFuture<?> future = mongoManager.getValue(...).thenAccept(...);
    batch.add(future);

    if (batch.size() >= RECONCILE_BATCH_SIZE) {
        waitForBatch(batch); // wait for all 50 to complete
        batch.clear();       // then start the next 50
    }
}

if (!batch.isEmpty()) waitForBatch(batch);

Enter fullscreen mode Exit fullscreen mode

private void waitForBatch(List<CompletableFuture<?>> batch) {
    try {
        CompletableFuture.allOf(batch.toArray(new CompletableFuture[0])).get();
    } catch (Exception e) {
        LOGGER.log(Level.WARNING, "[Reconciliation] Batch wait error", e);
    }
}

Enter fullscreen mode Exit fullscreen mode

At most 50 concurrent MongoDB requests are in flight at any time. The next batch only starts when the current one is done. This is easy to tune — bump RECONCILE_BATCH_SIZE if your MongoDB can handle more concurrency, lower it under constrained environments.


Problem 6: Redis Evict/TTL Was Silently Ignored

Redis can evict keys under memory pressure or when a TTL expires. The original L1 Sync did nothing when it detected a missing key.

The original code

redisManager.getData(key).ifPresent(redisJson -> {
    if (!redisJson.equals(model.getValueJson())) {
        model.setValueJson(redisJson);
    }
    // if getData() returns empty — we just skip it silently
    // L1 is now out of sync with an evicted Redis key
});

Enter fullscreen mode Exit fullscreen mode

When a key was evicted from Redis, L1 kept its stale value indefinitely. The next flush would try to read from Redis, get nothing, fall back to L1's stale data, and write that to MongoDB — potentially losing the newer value that was in Redis before eviction.

The fix

Optional<String> redisOpt = rm.getData(key);

if (redisOpt.isPresent()) {
    String redisJson = redisOpt.get();
    if (!redisJson.equals(model.getValueJson())) {
        model.setValueJson(redisJson); // L1 follows Redis
    }
} else {
    // Key was evicted from Redis — restore it from L1 and re-dirty
    LOGGER.warning("[L1Sync] Redis key missing, restoring: " + key);
    rm.setData(key, model.getValueJson()); // restore Redis from L1
    dirtyKeys.add(key);                    // trigger a Mongo re-write too
}

Enter fullscreen mode Exit fullscreen mode

When Redis doesn't have a key, L1's value is pushed back into Redis (restoring the master) and the key is marked dirty so the flush task re-persists it to MongoDB.


Problem 7: addModelFix() Left Redis Empty

This method was supposed to handle externally-provided data, but it only wrote to L1 — leaving Redis without the key.

The original code

public void addModelFix(String key, DataModel model) {
    idToDataList.put(key, model); // writes to L1 only
    dirtyKeys.add(key);
    // Redis has no entry for this key
    // Next L1 Sync will detect the missing Redis key, restore it from L1,
    // and re-dirty it — unnecessary extra cycle
}

Enter fullscreen mode Exit fullscreen mode

The fix

public void addModelFix(String key, DataModel model) {
    writeToL1AndRedis(key, model); // writes both L1 and Redis together
    dirtyKeys.add(key);
}

Enter fullscreen mode Exit fullscreen mode

All write paths now go through the same internal method, which guarantees both layers are always updated together.


Problem 8: O(n) ID Lookup

The getDataModelFromId() method streamed through the entire map on every call.

The original code

public Optional<DataModel> getDataModelFromId(String id) {
    return idToDataList.values().stream()
        .filter(dm -> dm.getId().equals(id))
        .findAny(); // O(n) — scans the whole map
}

Enter fullscreen mode Exit fullscreen mode

On 1000 entries this is 1000 comparisons per lookup. If this method is called frequently (e.g., per player action in a game server), it compounds fast.

The fix: reverse index

A second ConcurrentHashMap keeps a id → key mapping updated alongside the main map.

// new field
private final ConcurrentHashMap<String, String> idToKey;

// updated on every write
private void writeToL1AndRedis(String key, DataModel model) {
    keyToModel.put(key, model);
    idToKey.put(model.getId(), key); // maintain reverse index
    NexusApplication.getApplication().getRedisManager().setData(key, model.getValueJson());
}

// and on remove
public void removeModel(String key) {
    DataModel removed = keyToModel.remove(key);
    if (removed == null) return;
    idToKey.remove(removed.getId()); // keep reverse index clean
    // ...
}

// lookup is now O(1)
public Optional<DataModel> getDataModelFromId(String id) {
    String key = idToKey.get(id);
    if (key == null) return Optional.empty();
    return Optional.ofNullable(keyToModel.get(key));
}

Enter fullscreen mode Exit fullscreen mode

Two map lookups instead of a full scan. The memory cost is minimal — just a second map of strings.


The Final Picture

Here's a summary of every change made:

# Problem Impact Fix
1 Hierarchy inverted — Mongo was overriding Redis Wrong data served Redis fetched first; Mongo updated to match Redis
2 Dirty flag removed before Mongo write confirmed Silent data loss dirtyKeys.remove() called only after .get() succeeds
3 TOCTOU race in removeModel() Potential NPE / double-delete Single atomic remove() with return value check
4 Nested processTask in reconciliation Deadlock on single-thread executor All work in one task context, no inner dispatch
5 All entries queried simultaneously in reconciliation MongoDB connection storm Batch processing (50 at a time) with CompletableFuture.allOf()
6 Redis evict/TTL silently ignored in L1 Sync Stale L1 data, wrong Mongo writes Restore Redis from L1, mark dirty
7 addModelFix() skipped Redis Redis missing key, extra sync cycle Unified writeToL1AndRedis() for all write paths
8 getDataModelFromId() was O(n) CPU pressure on 1000+ entries id → key reverse index for O(1) lookup

Takeaway

The original code wasn't obviously broken — it ran, it synced, it mostly worked. The bugs were in the edge cases: what happens when Mongo is unavailable for one cycle, when Redis evicts a key under pressure, when two threads hit removeModel() at the same time.

Distributed cache management is one of those areas where the details really matter. A clear layer hierarchy and explicit failure guarantees aren't optional extras — they're what separate a system that works from one that almost always works.

The full source is available in the v1.1.0 release.


If you spotted something I missed or have a different approach to any of these problems, I'd love to hear it in the comments.