Redis 缓存穿透、缓存击穿和缓存雪崩是三种常见的缓存问题,它们可能导致系统性能下降甚至崩溃。
缓存穿透:是指请求查询缓存系统中不存在的数据,由于缓存不命中,请求会继续查询数据库
缓存穿透的原因通常有以下几点:
-
非法请求:攻击者故意构造不存在的请求(如使用随机生成的或异常的键值),试图绕过缓存层,直接对数据库进行攻击。
-
系统缺陷:系统设计时没有考虑到或没有正确处理查询不存在数据的情况。
-
数据缺失:合法请求查询的数据确实不存在,比如已删除或未生成的数据,但系统没有相应的缓存策略来处理这种情况。
原理
缓存穿透的原理可以通过以下步骤来解释:
-
客户端发起一个查询请求,请求中携带的键(key)用于在缓存中查找数据。
-
缓存系统接收到请求后,尝试根据提供的键查找对应的值。
-
如果缓存中不存在该键对应的值,缓存不命中,请求就会继续传递到数据库层。
-
数据库尝试查找请求的数据,但由于数据不存在,返回空结果。
-
系统可能会将空结果返回给客户端,但通常不会将这个空结果写入缓存。
-
如果有大量此类查询,每次查询都会穿透缓存直接对数据库进行,导致数据库压力增大。 实际例子:一个恶意攻击者不断地使用随机生成的用户 ID 来请求用户信息,因为这些用户 ID 不存在,所以每次请求都会直接查询数据库。
理论解决方案:
-
布隆过滤器:使用布隆过滤器(Bloom Filter)来检查请求的数据是否可能存在。如果布隆过滤器不存在,那么就可以直接返回,不需要查询数据库。
-
缓存空值:即使数据在数据库中不存在,也可以将一个特殊的空值或者标记存入缓存,以避免重复查询数据库。
-
接口限流:对接口进行限流操作,限制每个用户的请求频率。
具体例子:
解决方案1:布隆过滤器
假设我们有一个用户服务,我们可以使用 Google 的 Guava 库中的布隆过滤器来避免缓存穿透。
typescript
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class UserService {
private final BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 1000);
public User getUserById(Integer id) {
if (!bloomFilter.mightContain(id)) {
return null; // 如果布隆过滤器判断不存在,直接返回 null
}
// 正常的缓存逻辑
// ...
}
public void createUser(User user) {
// 创建用户的同时将用户 ID 加入布隆过滤器
bloomFilter.put(user.getId());
// 正常的创建用户逻辑
// ...
}
}
解决方案2:缓存空对象
kotlin
import org.springframework.cache.annotation.Cacheable;
public class UserService {
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Integer id) {
User user = findUserInDatabase(id);
if (user == null) {
// 如果数据库中也不存在,缓存一个空对象或特殊标记,设置较短的过期时间
cacheNullObject(id);
return null;
}
return user;
}
private void cacheNullObject(Integer id) {
// 缓存一个空对象或特殊标记
// ...
}
}
解决方案3:接口限流:使用 Google Guava RateLimiter 实现接口限流的 Java 示例代码。
java
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class RateLimiterUtil {
private static final ConcurrentMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
// 获取限流器,如果不存在则创建一个新的
public static RateLimiter getRateLimiter(String key, double permitsPerSecond) {
return limiters.computeIfAbsent(key, k -> RateLimiter.create(permitsPerSecond));
}
}
接口中使用这个限流器来控制请求:
java
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
// 假设每秒允许每个用户进行5次请求
private static final double PERMITS_PER_SECOND = 5.0;
@GetMapping("/user/{id}")
public User getUser(@PathVariable Integer id) {
RateLimiter rateLimiter = RateLimiterUtil.getRateLimiter("user_" + id, PERMITS_PER_SECOND);
// 检查是否能够立即获取令牌
if (!rateLimiter.tryAcquire()) {
throw new RateLimitExceededException("Rate limit exceeded. Please try again later.");
}
// 正常的业务逻辑
return findUserById(id);
}
private User findUserById(Integer id) {
// 查找用户的逻辑
return new User();
}
}
public class RateLimitExceededException extends RuntimeException {
public RateLimitExceededException(String message) {
super(message);
}
}
public class User {
// 用户类的实现
private long id;
private String userName;
private string address;
}
上面的代码中,我们为每个用户 ID 创建了一个单独的 RateLimiter
实例。当请求到达时,我们会尝试从限流器获取一个令牌。如果不能立即获取到令牌(表示请求太频繁),则抛出 RateLimitExceededException
异常。