Backend Development 7 min read

Redis Cache Avalanche and Mitigation Strategies with Semaphore Rate Limiting

The article explains how Redis cache expiration can cause a cache avalanche, describes typical scenarios, and presents mitigation techniques such as semaphore-based rate limiting and fault tolerance, accompanied by a Java service example demonstrating lock, semaphore, and cache handling.

Architect's Guide
Architect's Guide
Architect's Guide
Redis Cache Avalanche and Mitigation Strategies with Semaphore Rate Limiting

1. Redis Cache Expiration Leading to Avalanche

When the cache expires, a massive number of requests are redirected to the database, overwhelming it and causing the entire system to crash; dependent applications may also become unstable or fail.

High request volume makes the database unable to cope, resulting in a complete service collapse.

A single system failure propagates to other services that rely on it, leading to further instability or crashes.

2. Scenarios of Redis Cache Expiration

Maximum memory control: maxmemory sets the memory threshold; maxmemory-policy defines the eviction strategy when the threshold is reached.

3. Cache Avalanche Solutions

3.1 Semaphore Rate Limiting

The java.util.concurrent.Semaphore class is a key concurrency tool in the J.U.C package, used to control the number of threads that can acquire permits.

Core Methods acquire: obtain a permit, waiting if none are available. release: return a permit.

Typical scenario: limit concurrent processing of code.

Example code:

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

Additional strategies such as graceful degradation, fallback mechanisms, and circuit breakers can be combined with semaphore limiting to further protect the system during cache miss spikes.

backendJavaRedisSemaphoreRate LimitingCache Avalanche
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.