高并发场景下的缓存击穿问题探析与应对策略

目录

一、问题描述

二、解决策略分析

(一)解决策略一:互斥锁(Mutex)

(二)解决策略二:软过期+互斥锁

[(三)解决策略三:静态数据 + Lazy Expiration 方案](#(三)解决策略三:静态数据 + Lazy Expiration 方案)

三、总结


干货分享,感谢您的阅读!

在高并发场景下,缓存作为前置查询机制,显著减轻了数据库的压力,提高了系统性能。然而,这也带来了缓存失效、增加回溯率等风险。常见的问题包括缓存穿透、缓存雪崩、热Key和大Key等。这些问题如果不加以处理,会影响系统的稳定性和性能。因此,采用有效的缓存策略,如缓存空结果、布隆过滤器、缓存过期时间随机化、多级缓存等,对于保障系统在高并发情况下的可靠性至关重要。本次我们将详细探讨缓存击穿及其应对策略。

一、问题描述

在缓存系统中,有些数据可能会被频繁访问,这些数据被称为热点数据。为了保证缓存的有效性,缓存通常会设置一个过期时间。然而,当一个热点数据的缓存失效时,所有对该数据的请求会同时到达数据库。

这种情况会导致以下问题:

  1. 数据库压力大:当大量请求同时涌向数据库时,数据库的负载会瞬间增加,可能导致数据库性能下降,甚至崩溃。

  2. 无效的重复查询:所有请求都试图从数据库中读取相同的数据并更新缓存,这种重复的查询是无效的,因为只需要一次查询结果就能满足所有请求。

缓存击穿是由于热点数据的缓存失效导致的数据库压力过大问题。为了解决这个问题,可以采用互斥锁、永不过期和提前预热缓存等方法。这些方法各有优缺点,可以根据具体业务场景选择合适的解决方案,以确保系统在高并发访问下的稳定性和高性能。

二、解决策略分析

(一)解决策略一:互斥锁(Mutex)

在缓存失效时,通过加锁机制保证只有一个请求能访问数据库,其余请求等待该请求完成后再返回缓存数据。具体实现可以使用分布式锁(如 Redis 的分布式锁)。这种方式可以有效防止缓存击穿,但需要注意加锁的开销。

java 复制代码
public Object getCache(final String key) {
    Object value = redis.get(key);
    if (value != null) {
        return value;
    }
    synchronized (this) {
        value = redis.get(key);
        if (value != null) {
            return value;
        }
        value = getValueFromDb(key);
        redis.set(key, value, expireTime);
    }
    return value;
}

(二)解决策略二:软过期+互斥锁

软过期指的是在缓存值中存储一个逻辑过期时间,这个时间比实际要过期的时间小(即 < )。在业务取值时,首先校验 是否过期,如果过期,则引入互斥锁。首先将 时间延长(即 =+)并设置到缓存中,然后去数据库查询新数据。其他线程这时看到延长了的过期时间,就会继续使用旧数据,等获取最新数据的线程更新缓存后,所有线程都会使用新的数据。

相比单纯的互斥锁方案,这种方案的优点在于进一步减少了读请求线程的阻塞时间。

java 复制代码
public class CacheService {

    @Autowired
    private RedisTemplate<String, CacheValue> redisTemplate;

    @Autowired
    private DatabaseService databaseService;

    private static final String MUTEX_KEY_SUFFIX = ":mutex";

    public CacheValue getCache(final String key) {
        CacheValue value = redisTemplate.opsForValue().get(key);
        long currentTime = System.currentTimeMillis();

        if (value != null) {
            // 检查缓存中的逻辑过期时间
            if (value.getTimeout() <= currentTime) {
                // 如果逻辑过期时间已过期,尝试获取互斥锁
                if (redisTemplate.opsForValue().setIfAbsent(key + MUTEX_KEY_SUFFIX, currentTime + 30000)) {
                    // 立即延长逻辑过期时间,减少阻塞时间
                    value.setTimeout(currentTime + 5000);
                    redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(1));

                    // 获取最新的数据库数据,并重新设置新的逻辑过期时间,覆盖旧数据
                    CacheValue newValue = databaseService.getValueFromDb(key);
                    newValue.setTimeout(currentTime + 60000);
                    redisTemplate.opsForValue().set(key, newValue, Duration.ofMinutes(1));
                    
                    // 删除互斥锁
                    redisTemplate.delete(key + MUTEX_KEY_SUFFIX);
                } else {
                    // 如果没有获取到互斥锁,等待一段时间后重试
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    return getCache(key);
                }
            }
            return value;
        } else {
            // 缓存不存在的情况,与上面类似
            if (redisTemplate.opsForValue().setIfAbsent(key + MUTEX_KEY_SUFFIX, currentTime + 30000)) {
                CacheValue newValue = databaseService.getValueFromDb(key);
                newValue.setTimeout(currentTime + 60000);
                redisTemplate.opsForValue().set(key, newValue, Duration.ofMinutes(1));
                
                // 删除互斥锁
                redisTemplate.delete(key + MUTEX_KEY_SUFFIX);
            } else {
                // 如果没有获取到互斥锁,等待一段时间后重试
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                return getCache(key);
            }
        }
        return value;
    }

    // 定义缓存值的类
    public static class CacheValue {
        private Object data;
        private long timeout;

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public long getTimeout() {
            return timeout;
        }

        public void setTimeout(long timeout) {
            this.timeout = timeout;
        }
    }
}

通过软过期+互斥锁的方案,可以有效防止缓存击穿问题,并减少读请求线程的阻塞时间。合理设置逻辑过期时间和互斥锁的过期时间,结合重试机制,可以提高系统在高并发场景下的稳定性和性能。

(三)解决策略三:静态数据 + Lazy Expiration 方案

静态数据缓存策略指的是在 Redis 中不设置过期时间(TTL),让数据看起来是"永不过期"的。实际上,通过在缓存值中设置逻辑过期时间来控制数据的有效性。当逻辑过期时间到达时,后台异步线程会更新缓存。这种方式性能最好,因为避免了频繁的缓存失效和重建。

具体实现

  1. 逻辑过期时间:在缓存值中设置一个逻辑过期时间,当取值时判断逻辑过期时间是否已经到达。
  2. 异步更新:如果逻辑过期时间已过,则启动一个异步线程来更新缓存,而不是阻塞当前请求。
  3. 互斥锁:避免多线程同时更新缓存,使用互斥锁来保证只有一个线程能进行更新操作。
java 复制代码
public class CacheService {

    @Autowired
    private RedisTemplate<String, CacheValue> redisTemplate;

    @Autowired
    private DatabaseService databaseService;

    @Autowired
    private ExecutorService executorService;

    private static final String MUTEX_KEY_SUFFIX = ":mutex";

    public CacheValue getCache(final String key) {
        CacheValue value = redisTemplate.opsForValue().get(key);
        long currentTime = System.currentTimeMillis();

        if (value != null) {
            // 检查缓存中的逻辑过期时间
            if (value.getTimeout() <= currentTime) {
                // 另起一个异步线程更新缓存
                executorService.execute(() -> {
                    if (redisTemplate.opsForValue().setIfAbsent(key + MUTEX_KEY_SUFFIX, "1")) {
                        redisTemplate.expire(key + MUTEX_KEY_SUFFIX, Duration.ofMinutes(3));
                        CacheValue dbValue = databaseService.getValueFromDb(key);
                        dbValue.setTimeout(currentTime + 60000); // 设置新的逻辑过期时间
                        redisTemplate.opsForValue().set(key, dbValue);
                        redisTemplate.delete(key + MUTEX_KEY_SUFFIX);
                    }
                });
            }
            return value;
        } else {
            // 缓存不存在的情况,与上面类似
            if (redisTemplate.opsForValue().setIfAbsent(key + MUTEX_KEY_SUFFIX, "1")) {
                CacheValue dbValue = databaseService.getValueFromDb(key);
                dbValue.setTimeout(currentTime + 60000); // 设置逻辑过期时间
                redisTemplate.opsForValue().set(key, dbValue);
                redisTemplate.delete(key + MUTEX_KEY_SUFFIX);
            } else {
                // 如果没有获取到互斥锁,等待一段时间后重试
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                return getCache(key);
            }
        }
        return value;
    }

    // 定义缓存值的类
    public static class CacheValue {
        private Object data;
        private long timeout;

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public long getTimeout() {
            return timeout;
        }

        public void setTimeout(long timeout) {
            this.timeout = timeout;
        }
    }
}

通过静态数据 + Lazy Expiration 方案,可以有效防止缓存击穿问题,同时避免了频繁的缓存失效和重建,提升系统的性能与稳定性。

三、总结

在高并发场景中,缓存的有效性对于系统性能和稳定性至关重要。缓存击穿问题是由于热点数据缓存失效导致的数据库压力过大现象,为了解决这一问题,可以采取多种策略。

  • 互斥锁机制通过加锁确保只有一个请求能访问数据库,防止同时访问造成的压力;
  • 软过期+互斥锁结合逻辑过期时间和分布式锁,减少读请求线程的阻塞时间;
  • 静态数据+Lazy Expiration策略则通过逻辑过期时间和异步更新机制,避免频繁的缓存失效和重建。

通过合理选择和实施缓存策略,系统可以更好地应对高并发场景下的挑战,提升整体性能和用户体验。

相关推荐
Steadfast_GG5 分钟前
Redis中的通用命令
redis·缓存
小二·12 分钟前
Redis 内存溢出(OOM)排查与恢复实战
数据库·redis·bootstrap
pqk6V6Vep13 分钟前
Redis 分布式锁进阶第一篇讲解
数据库·redis·分布式
giaz14n9X29 分钟前
Redis 分布式锁进阶第六十一篇
数据库·redis·分布式
洛水水2 小时前
消息队列与Kafka详解
分布式·kafka
鸿乃江边鸟3 小时前
Spark中怎么做Spark canonicalize归一化
大数据·分布式·spark
JAVA面经实录9174 小时前
Redis 知识体系(完整版)
java·redis·nosql数据库·nosql
SLD_Allen4 小时前
Kafka分区与消费者的关系kafka分区和消费者线程的关系
分布式·kafka
he___H4 小时前
数据密集型应用系统设计--其一
分布式
颜笑晏晏4 小时前
长输入短输出场景下的 SGLang 推理性能实测前缀缓存、PD 分离配比与参数调优
缓存·推理优化·sglang·ai infra·pd分离