20.缓存问题与解决方案详解教程

发布于:2025-07-16 ⋅ 阅读:(16) ⋅ 点赞:(0)

文章目录

1. 缓存基础概念

1.1 什么是缓存

缓存是一种高速存储技术,用于临时存储频繁访问的数据,以提高系统性能和响应速度。在软件架构中,缓存通常位于应用程序和数据库之间,作为数据的快速访问层。

1.2 缓存的作用

  • 提高响应速度:从内存中读取数据比从磁盘快几个数量级
  • 减少数据库压力:减少对数据库的直接访问
  • 提升用户体验:快速响应用户请求
  • 节约成本:减少服务器资源消耗

1.3 常见的缓存类型

  • 本地缓存:如HashMap、Guava Cache
  • 分布式缓存:如Redis、Memcached
  • 数据库缓存:如MySQL查询缓存
  • CDN缓存:内容分发网络缓存

1.4 缓存架构示例

// 典型的缓存使用模式
public class UserService {
   
    private RedisTemplate<String, Object> redisTemplate;
    private UserRepository userRepository;
    
    public User getUserById(Long userId) {
   
        String key = "user:" + userId;
        
        // 1. 先查缓存
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
   
            return user; // 缓存命中
        }
        
        // 2. 缓存未命中,查数据库
        user = userRepository.findById(userId);
        if (user != null) {
   
            // 3. 将数据写入缓存
            redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
        }
        
        return user;
    }
}

2. 缓存雪崩 (Cache Avalanche)

2.1 什么是缓存雪崩

缓存雪崩是指在同一时间,大量的缓存key同时失效,导致大量请求直接打到数据库上,造成数据库瞬间压力过大甚至宕机的现象。

2.2 缓存雪崩的原因

  1. 缓存服务器宕机:Redis服务器突然宕机
  2. 大量key同时过期:设置了相同的过期时间
  3. 缓存预热不充分:系统重启后缓存为空

2.3 缓存雪崩的危害

  • 数据库瞬间压力暴增
  • 系统响应时间急剧增加
  • 可能导致数据库连接池耗尽
  • 严重时可能导致整个系统崩溃

2.4 缓存雪崩的解决方案

方案1:设置随机过期时间
@Service
public class ProductService {
   
    private RedisTemplate<String, Object> redisTemplate;
    private ProductRepository productRepository;
    
    public Product getProductById(Long productId) {
   
        String key = "product:" + productId;
        Product product = (Product) redisTemplate.opsForValue().get(key);
        
        if (product == null) {
   
            product = productRepository.findById(productId);
            if (product != null) {
   
                // 设置随机过期时间:30分钟 + 0-10分钟的随机时间
                int randomMinutes = new Random().nextInt(10);
                Duration expireTime = Duration.ofMinutes(30 + randomMinutes);
                redisTemplate.opsForValue().set(key, product, expireTime);
            }
        }
        
        return product;
    }
}
方案2:缓存集群和主从复制
# Redis集群配置示例
spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.100:7001
        - 192.168.1.100:7002
        - 192.168.1.100:7003
        - 192.168.1.101:7001
        - 192.168.1.101:7002
        - 192.168.1.101:7003
      max-redirects: 3
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 16
        max-idle: 8
        min-idle: 0
方案3:熔断降级机制
@Component
public class ProductServiceWithCircuitBreaker {
   
    private RedisTemplate<String, Object> redisTemplate;
    private ProductRepository productRepository;
    private CircuitBreaker circuitBreaker;
    
    public ProductServiceWithCircuitBreaker() {
   
        // 配置熔断器
        this.circuitBreaker = CircuitBreaker.ofDefaults("productService");
        circuitBreaker.getEventPublisher().onStateTransition(event ->
            System.out.println("CircuitBreaker state transition: " + event));
    }
    
    public Product getProductById(Long productId) {
   
        return circuitBreaker.executeSupplier(() -> {
   
            String key = "product:" + productId;
            Product product = (Product) redisTemplate.opsForValue().get(key);
            
            if (product == null) {
   
                product = productRepository.findById(productId);
                if (product != null) {
   
                    redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
                }
            }
            
            return product;
        });
    }
}
方案4:本地缓存兜底
@Service
public class ProductServiceWithLocalCache {
   
    private RedisTemplate<String, Object> redisTemplate;
    private ProductRepository productRepository;
    private Cache<String, Product> localCache;
    
    public ProductServiceWithLocalCache() {
   
        // 创建本地缓存
        this.localCache = Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .build();
    }
    
    public Product getProductById(Long productId) {
   
        String key = "product:" + productId;
        
        try {
   
            // 1. 先查Redis缓存
            Product product = (Product) redisTemplate.opsForValue().get(key);
            if (product != null) {
   
                // 同时更新本地缓存
                localCache.put(key, product);
                return product;
            }
        } catch (Exception e) {
   
            // Redis异常时,查询本地缓存
            Product localProduct = localCache.getIfPresent(key);
            if (localProduct != null) {
   
                return localProduct;
            }
        }
        
        // 2. 查询数据库
        Product product = productRepository.findById(productId);
        if (product != null) {
   
            try {
   
                redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
            } catch (Exception e) {
   
                // Redis写入失败,只更新本地缓存
                localCache.put(key, product);
            }
        }
        
        return product;
    }
}

2.5 缓存雪崩预防最佳实践

@Configuration
public class CacheConfiguration {
   
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
   
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // 设置序列化方式
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        return template;
    }
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
   
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)) // 默认30分钟过期
                .disableCachingNullValues();
        
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

3. 缓存穿透 (Cache Penetration)

3.1 什么是缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时才查询数据库,而且不存在的数据不会写入缓存,导致这个不存在的数据每次请求都要查询数据库,给数据库造成压力。

3.2 缓存穿透的场景示例

// 问题代码示例
public User getUserById(Long userId) {
   
    String key = "user:" + userId;
    
    // 1. 查缓存
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user != null) {
   
        return user;
    }
    
    // 2. 查数据库
    user = userRepository.findById(userId);
    if (user != null) {
   
        // 3. 只有数据存在才缓存
        redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
    }
    // 如果user为null,不缓存,下次还会查数据库
    
    return user;
}

3.3 缓存穿透的危害

  • 大量无效请求穿透到数据库
  • 数据库查询压力增大
  • 系统整体性能下降
  • 可能被恶意攻击利用

3.4 缓存穿透的解决方案

方案1:缓存空值
@Service
public class UserServiceWithNullCache {
   
    private RedisTemplate<String, Object> redisTemplate;
    private UserRepository userRepository;
    
    public User getUserById(Long userId) {
   
        String key = "user:" + userId;
        
        // 1. 查缓存
        Object cached = redisTemplate.opsForValue().get(key);
        if (cached != null) {
   
            // 如果是特殊标记,说明数据不存在
            if ("NULL".equals(cached)) {
   
                return null;
            }
            return (User) cached;
        }
        
        // 2. 查数据库
        User user = userRepository.findById(userId);
        if (user != null) {
   
            // 3. 缓存有效数据
            redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
        } else {
   
            // 4. 缓存空值,但设置较短的过期时间
            redisTemplate.opsForValue().set(key, "NULL", Duration.ofMinutes(5));
        }
        
        return user;
    }
}
方案2:布隆过滤器
@Service
public class UserServiceWithBloomFilter {
   
    private RedisTemplate<String, Object> redisTemplate;
    private UserRepository userRepository;
    private BloomFilter<Long> bloomFilter;
    
    @PostConstruct
    public void initBloomFilter() {
   
        // 创建布隆过滤器,预计100万个元素,误判率0.01%
        bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.0001);
        
        // 将所有用户ID加入布隆过滤器
        List<Long> userIds = userRepository.findAllUserIds();
        userIds.forEach(bloomFilter::put);
    }
    
    public User getUserById(Long userId) {
   
        // 1. 先用布隆过滤器判断
        if (!bloomFilter.mightContain(userId)) {
   
            // 布隆过滤器说不存在,一定不存在
            return null;
        }
        
        String key = "user:" + userId;
        
        // 2. 查缓存
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
   
            return user;
        }
        
        // 3. 查数据库
        user = userRepository.findById(userId);
        if (user != null) {
   
            redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
        }
        
        return user;
    }
}
方案3:参数校验
@RestController
public class UserController {
   
    private UserService userService;
    
    @GetMapping("/user/{userId}")
    public ResponseEntity<User> getUser(@PathVariable Long userId) {
   
        // 1. 参数校验
        if (userId == null || userId <= 0) {
   
            return ResponseEntity.badRequest().build();
        }
        
        // 2. 业务范围校验
        if (userId > 999999999L) {
    // 假设用户ID不会超过这个值
            return ResponseEntity.notFound().build();
        }
        
        User user 

网站公告

今日签到

点亮在社区的每一天
去签到