Backend Development 8 min read

Redis Cache Expiration Avalanche and Mitigation Strategies

The article explains how Redis cache expiration can cause a request avalanche that overloads databases, and presents mitigation techniques such as semaphore rate limiting, per‑key locking, fault‑tolerant cache rebuilding, and example Java code using Spring and Redis.

Top Architect
Top Architect
Top Architect
Redis Cache Expiration Avalanche and Mitigation Strategies

The article discusses the problem of Redis cache expiration causing a cascade of requests to the database, known as a cache avalanche, and offers practical solutions.

1. Redis Data Expiration Avalanche

When the cache expires, massive requests are redirected to the database, leading to overload, system collapse, and instability of dependent services.

2. Scenarios of Redis Data Expiration

Key Redis parameters such as maxmemory and maxmemory-policy determine behavior when memory thresholds are reached.

3. Cache Avalanche Mitigation Solutions

3.1 Semaphore Rate Limiting

Using Java's java.util.concurrent.Semaphore to limit concurrent accesses, combined with per‑key ReentrantLock to avoid duplicate database queries.

package cn.lazyfennec.cache.redis.service;

import cn.lazyfennec.cache.redis.annotations.NeteaseCache;
import cn.lazyfennec.cache.redis.dao.UserDao;
import cn.lazyfennec.cache.redis.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

@Service // 默认 单实例
public class UserService2 {

    @Autowired
    UserDao userDao;

    @Autowired
    RedisTemplate redisTemplate; // spring提供的一个redis客户端,底层封装了jedis等客户端

    // userId ---> lock 记录每一个userId当前的查询情况
    static Map
mapLock = new ConcurrentHashMap<>();

    static Semaphore semaphore = new Semaphore(50); // 信号量 50 -- 类似车票

    /**
     * 根据ID查询用户信息 (redis缓存,用户信息以json字符串格式存在(序列化))
     */
    public User findUserById(String userId) throws Exception {

        // 1. 先读取缓存
        Object cacheValue = redisTemplate.opsForValue().get(userId); // redisTemplate是spring提供的redis客户端
        if (cacheValue != null) {
            System.out.println("###缓存命中:" + ((User) cacheValue).getUname());
            return (User) cacheValue;
        }

        // ---------------缓存miss之后流程--------------
        ReentrantLock reentrantLock = new ReentrantLock();
        try {
            if (mapLock.putIfAbsent(userId, reentrantLock) != null) { // 有返回值代表存在锁
                reentrantLock = mapLock.get(userId);
            }
            Thread.sleep(3000); // TODO 停顿3秒,等下一个线程过来,模拟多个用户同时并发请求的场景
            reentrantLock.lock(); // 争抢锁,抢不到的排队---1个请求查询数据库 --- 599个等待
            Thread.sleep(3000); // TODO 停顿3秒,模拟lock获取之后业务处理时间

            // 再次查询缓存 -- 避免大量重复数据库查询
            cacheValue = redisTemplate.opsForValue().get(userId); // redisTemplate是spring提供的redis客户端
            if (cacheValue != null) {
                System.out.println("###缓存命中:" + ((User) cacheValue).getUname());
                return (User) cacheValue;
            }

            semaphore.acquire(); // 获取信号量 没有获取到

            // 2. 如果缓存miss,则查询数据库
            User user = userDao.findUserById(userId);
            System.out.println("***缓存miss:" + user.getUname());
            // 3. 设置缓存(重建缓存) // 主播信息查询缓存
            redisTemplate.opsForValue().set(userId, user); // set key value
            redisTemplate.expire(userId, 100, TimeUnit.SECONDS); // 需要手动设

            semaphore.release(); // 释放信号量

            return user;
        } finally {
            if (!reentrantLock.hasQueuedThreads()) { // 当锁最后一个释放的时候,删除掉
                mapLock.remove(userId);
            }
            reentrantLock.unlock();
        }

    }

    @CacheEvict(value = "user", key = "#user.uid") // 方法执行结束,清除缓存
    public void updateUser(User user) {
        String sql = "update tb_user_base set uname = ? where uid=?";
        jdbcTemplate.update(sql, new String[]{user.getUname(), user.getUid()});
    }

    /**
     * 根据ID查询用户名称
     */
    // 我自己实现一个类似的注解
    @NeteaseCache(value = "uname", key = "#userId") // 缓存
    public String findUserNameById(String userId) {
        // 查询数据库
        String sql = "select uname from tb_user_base where uid=?";
        String uname = jdbcTemplate.queryForObject(sql, new String[]{userId}, String.class);

        return uname;
    }

    @Autowired
    JdbcTemplate jdbcTemplate; // spring提供jdbc一个工具(mybastis类似)
}

3.2 Fault Tolerance and Degradation

Applying the cache‑aside pattern, setting explicit expiration times, and using annotations such as @CacheEvict and a custom @NeteaseCache to manage cache lifecycle and ensure graceful degradation under high load.

JavacacheRedisSpringSemaphoreAvalanche
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login 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.