Why Delayed Double Delete Fails: A Hierarchical Java Design for MySQL‑Redis Consistency

The article analyzes why MySQL and Redis cannot guarantee strong consistency with simple cache‑aside patterns, explains the pitfalls of delayed double delete, and presents a tiered Java design—including transaction‑after‑commit deletion, retryable invalidation, CDC/Outbox pipelines, TTL safeguards, and multi‑level cache considerations—to achieve reliable cache consistency.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Why Delayed Double Delete Fails: A Hierarchical Java Design for MySQL‑Redis Consistency

Why MySQL and Redis Cannot Be Absolutely Consistent

Database updates and Redis cache operations are separate transactional resources. A successful MySQL commit does not guarantee a successful Redis deletion, and a successful Redis deletion does not guarantee a successful MySQL commit. Therefore any design must assume partial failures.

Cache‑Aside Pattern and Update Order

Typical read flow:

Read Redis → if miss, read MySQL → write result to Redis

Two update strategies:

Delete cache first, then update DB – a concurrent read can fetch the old DB value and rewrite it to Redis, leaving a long‑lived stale cache.

Update DB first, then delete cache – reduces the probability of stale data and is generally preferred, though not absolutely safe.

Why Updating the DB First Then Deleting the Cache Can Still Fail

Example with two threads when the cache is initially empty:

Thread A reads Redis (miss), reads old DB value 100, then blocks before writing to Redis.

Thread B updates DB to 80 and deletes the Redis key.

Thread A resumes and writes the stale value 100 back to Redis.

Result: MySQL=80, Redis=100. The window is narrow but demonstrates that “update‑then‑delete” is not foolproof.

Delayed Double Delete

public void updateProduct(Long productId, BigDecimal price) {
    String key = "product:" + productId;
    redisTemplate.delete(key);               // first delete
    productRepository.updatePrice(productId, price); // DB update
    delayedExecutor.schedule(() -> redisTemplate.delete(key), 500, TimeUnit.MILLISECONDS);
}

The first delete reduces the chance of reading stale data, the DB update persists the new state, and the delayed second delete attempts to remove any old value that might have been written back by a concurrent read. Remaining problems:

The delay (e.g., 500 ms) is hard to tune; slow DB queries or GC pauses can exceed it.

If the application restarts before the delayed task runs, the second delete never happens.

The approach still depends on Redis deletion succeeding.

Thus delayed double delete is a mitigation, not a strict consistency guarantee.

Database Update and Cache Deletion in the Same @Transactional Block Is Insufficient

@Transactional
public void updateProduct(Long productId, BigDecimal price) {
    productRepository.updatePrice(productId, price);
    redisTemplate.delete("product:" + productId);
}

Spring’s @Transactional manages only the database connection; Redis operations execute outside the DB transaction. If Redis deletion succeeds but the DB transaction later rolls back, the cache remains empty while the DB reverts, breaking consistency. The safe practice is to delete the cache **after** the DB transaction commits, using Spring’s transaction synchronization:

@Transactional
public void updateProduct(Long productId, BigDecimal price) {
    productRepository.updatePrice(productId, price);
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
        @Override
        public void afterCommit() {
            redisTemplate.delete("product:" + productId);
        }
    });
}

Cache Deletion Must Be Retryable

Many projects simply log a failure of redisTemplate.delete(key) and give up, which is unsafe when the DB update succeeded. A reliable pattern writes a compensation record into a table when deletion fails and processes it later.

CREATE TABLE cache_invalidation_task (
    id BIGINT PRIMARY KEY,
    cache_key VARCHAR(255) NOT NULL,
    status VARCHAR(32) NOT NULL,
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_time DATETIME,
    created_at DATETIME NOT NULL
);
@Transactional
public void updateProduct(Long productId, BigDecimal price) {
    productRepository.updatePrice(productId, price);
    cacheTaskRepository.create("product:" + productId);
}

A background worker scans pending tasks and retries deletion, guaranteeing that a successful DB update always leaves a traceable invalidation task.

More Mature Solution: Binlog or CDC‑Driven Cache Invalidation

Decouple business logic from cache updates by publishing change events from MySQL binlog (Canal/Debezium) to a message queue, then letting a consumer delete the corresponding Redis key:

MySQL UPDATE → Binlog → CDC → MQ → Cache‑Invalidation Consumer → Redis DELETE
@KafkaListener(topics = "product-change")
public void handle(ProductChangeEvent event) {
    redisTemplate.delete("product:" + event.id());
}

This guarantees that cache deletion occurs only after the DB change is persisted, and the consumer can be scaled and retried independently. The trade‑off is added infrastructure complexity.

Cache Consumers Must Be Idempotent

// First delete removes the key
// Second delete finds the key already absent

Deleting a key is naturally idempotent, so repeated consumption of the same invalidation message does not cause inconsistency.

Strict Consistency Should Not Rely on Cache

For critical data such as account balances, inventory deductions, coupon usage, payment status, or permission changes, the cache must never be the final source of truth. The database should be consulted for the definitive answer; the cache may be used only for display or performance acceleration.

Design Consistency by Business Tier

Tier 1 – minute‑level tolerance (e.g., article view counts, leaderboards). Simple TTL is sufficient.

Tier 2 – second‑level tolerance (e.g., product details, user profile). Use update‑then‑delete with retry and reasonable TTL.

Tier 3 – fast eventual consistency (e.g., price, order status). Combine DB transaction, outbox/compensation table, MQ, and cache‑invalidator; keep the inconsistency window within seconds.

Tier 4 – strict consistency (e.g., balance deduction, final inventory). Cache must not be used for final decisions; rely on DB or strongly consistent storage.

TTL Is the Last Line of Defense

Even with robust invalidation mechanisms, a failed delete can leave stale data forever. Setting a reasonable TTL (e.g., 30 minutes) ensures eventual expiration. To avoid a cache‑snowball, stagger TTLs with a random offset:

long seconds = 1800 + ThreadLocalRandom.current().nextLong(300);
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(seconds));

Prevent Cache Stampede During Rebuild

When a cache miss triggers many concurrent DB reads, use a distributed lock (e.g., Redisson) and double‑check the cache after acquiring the lock:

public ProductVO getProduct(Long productId) {
    String key = "product:" + productId;
    ProductVO value = getFromCache(key);
    if (value != null) return value;
    RLock lock = redissonClient.getLock("lock:" + key);
    try {
        lock.lock();
        value = getFromCache(key);
        if (value != null) return value;
        value = productRepository.queryProduct(productId);
        writeCache(key, value);
        return value;
    } finally {
        lock.unlock();
    }
}

Logical Expiration for Hot Data

Store an explicit expireAt timestamp inside the cached object while keeping the Redis key alive. When a read finds the logical expiration, the first thread acquires a lock to rebuild the cache; other threads continue serving the stale value temporarily.

Multi‑Level Cache Increases Consistency Complexity

Beyond Redis, systems may have local Caffeine caches, CDN caches, gateway caches, and browser caches. Updating the DB and deleting Redis is insufficient; all layers must be invalidated, typically via a broadcast event after the DB commit.

Cache‑Key Design Affects Invalidation Difficulty

Cache single objects; keep list caches as ID arrays so that a price change only requires deleting the object key.

Versioned keys (e.g., product:list:v12:category:10) allow bulk invalidation by bumping the version.

For complex queries, use short TTLs instead of precise invalidation.

Recommended Solution for Typical Spring Boot Projects

Read: Cache‑Aside.

Update: Update DB → after‑commit delete Redis (using transaction synchronization).

Guarantee: Write failed deletions to compensation tasks.

Fallback: Set TTL on all caches.

Hotspot: Mutex rebuild or logical expiration.

Critical business: Never use cache as the final decision source.

For larger scales, extend to:

MySQL → Binlog / Outbox → MQ → Cache‑Invalidation Consumer → Redis / Local Cache

Six Questions to Answer Before Choosing a Cache Strategy

Maximum acceptable staleness for the data?

How to handle cache‑delete failures?

Can the cache be used as the final business decision?

Will cache rebuild cause DB stampede?

Is there multi‑level caching?

How to monitor inconsistency (failed deletions, task backlog, hit rate)?

Conclusion

There is no universal formula for MySQL‑Redis consistency. Deleting the cache before updating the DB is prone to stale data; updating the DB first reduces risk but still requires reliable post‑commit deletion, retry mechanisms, and possibly CDC/Outbox pipelines. The key is to assess business tolerance for inconsistency, design appropriate fallback and compensation paths, and avoid treating cache as the source of truth for critical operations.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRedisSpringMySQLConsistencyCDCCache Invalidation
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.