目录
[(三)解决策略三:静态数据 + Lazy Expiration 方案](#(三)解决策略三:静态数据 + Lazy Expiration 方案)
干货分享,感谢您的阅读!
在高并发场景下,缓存作为前置查询机制,显著减轻了数据库的压力,提高了系统性能。然而,这也带来了缓存失效、增加回溯率等风险。常见的问题包括缓存穿透、缓存雪崩、热Key和大Key等。这些问题如果不加以处理,会影响系统的稳定性和性能。因此,采用有效的缓存策略,如缓存空结果、布隆过滤器、缓存过期时间随机化、多级缓存等,对于保障系统在高并发情况下的可靠性至关重要。本次我们将详细探讨缓存击穿及其应对策略。
一、问题描述
在缓存系统中,有些数据可能会被频繁访问,这些数据被称为热点数据。为了保证缓存的有效性,缓存通常会设置一个过期时间。然而,当一个热点数据的缓存失效时,所有对该数据的请求会同时到达数据库。

这种情况会导致以下问题:
-
数据库压力大:当大量请求同时涌向数据库时,数据库的负载会瞬间增加,可能导致数据库性能下降,甚至崩溃。
-
无效的重复查询:所有请求都试图从数据库中读取相同的数据并更新缓存,这种重复的查询是无效的,因为只需要一次查询结果就能满足所有请求。
缓存击穿是由于热点数据的缓存失效导致的数据库压力过大问题。为了解决这个问题,可以采用互斥锁、永不过期和提前预热缓存等方法。这些方法各有优缺点,可以根据具体业务场景选择合适的解决方案,以确保系统在高并发访问下的稳定性和高性能。
二、解决策略分析
(一)解决策略一:互斥锁(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),让数据看起来是"永不过期"的。实际上,通过在缓存值中设置逻辑过期时间来控制数据的有效性。当逻辑过期时间到达时,后台异步线程会更新缓存。这种方式性能最好,因为避免了频繁的缓存失效和重建。
具体实现
- 逻辑过期时间:在缓存值中设置一个逻辑过期时间,当取值时判断逻辑过期时间是否已经到达。
- 异步更新:如果逻辑过期时间已过,则启动一个异步线程来更新缓存,而不是阻塞当前请求。
- 互斥锁:避免多线程同时更新缓存,使用互斥锁来保证只有一个线程能进行更新操作。
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策略则通过逻辑过期时间和异步更新机制,避免频繁的缓存失效和重建。
通过合理选择和实施缓存策略,系统可以更好地应对高并发场景下的挑战,提升整体性能和用户体验。