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

目录

一、问题描述

二、解决策略分析

(一)解决策略一:互斥锁(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策略则通过逻辑过期时间和异步更新机制,避免频繁的缓存失效和重建。

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

相关推荐
Li_7695322 小时前
Redis进阶(二)—— Redis 事务
数据库·redis·缓存
极客小云3 小时前
【Dockerfile 编写最佳实践:优化镜像构建与层缓存】
缓存·docker·k8s
CodeAmaz3 小时前
Redis大Key与热点Key问题解决方案
redis·大key·热点key
java1234_小锋4 小时前
Redis是单线程还是多线程?
数据库·redis·缓存
云计算-Security4 小时前
基于 Keepalived 的 Redis 主备高可用架构设计与实现
redis·keepalived
柒.梧.4 小时前
深度解析MyBatis缓存机制:从基础原理到实战配置
缓存·mybatis
222you4 小时前
在云服务器上配置redis环境(OpenCloudOS)
数据库·redis·缓存
Wang's Blog4 小时前
Kafka: 生产者客户端工作机制深度解析
分布式·kafka
一直都在5725 小时前
MyBatis缓存
缓存·mybatis