Redis三大缓存问题-缓存穿透原因原理及代码解决方案

Redis 缓存穿透、缓存击穿和缓存雪崩是三种常见的缓存问题,它们可能导致系统性能下降甚至崩溃。

缓存穿透:是指请求查询缓存系统中不存在的数据,由于缓存不命中,请求会继续查询数据库

缓存穿透的原因通常有以下几点:

  1. 非法请求:攻击者故意构造不存在的请求(如使用随机生成的或异常的键值),试图绕过缓存层,直接对数据库进行攻击。

  2. 系统缺陷:系统设计时没有考虑到或没有正确处理查询不存在数据的情况。

  3. 数据缺失:合法请求查询的数据确实不存在,比如已删除或未生成的数据,但系统没有相应的缓存策略来处理这种情况。

原理

缓存穿透的原理可以通过以下步骤来解释:

  1. 客户端发起一个查询请求,请求中携带的键(key)用于在缓存中查找数据。

  2. 缓存系统接收到请求后,尝试根据提供的键查找对应的值。

  3. 如果缓存中不存在该键对应的值,缓存不命中,请求就会继续传递到数据库层。

  4. 数据库尝试查找请求的数据,但由于数据不存在,返回空结果。

  5. 系统可能会将空结果返回给客户端,但通常不会将这个空结果写入缓存。

  6. 如果有大量此类查询,每次查询都会穿透缓存直接对数据库进行,导致数据库压力增大。 实际例子:一个恶意攻击者不断地使用随机生成的用户 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 异常。

相关推荐
Lee川8 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川11 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i13 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有13 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有13 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫14 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫14 小时前
Handler基本概念
面试
Wect15 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼16 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼16 小时前
Next.js 企业级落地
前端·javascript·面试