Spring Boot 项目中的潜在隐患:缓存策略不当引发的内存泄漏与性能退化

大家好!今天来聊一个在 SpringBoot 项目中经常被忽视但又极其重要的问题:缓存策略不当。不知道你是否遇到过这样的情况:系统上线初期一切正常,随着时间推移和数据量增加,应用突然变得越来越慢,甚至出现内存溢出(OOM)?很可能,你的缓存策略出了问题!

一、常见的缓存陷阱

1. 无限制缓存:最常见的内存问题

案例:我曾参与一个电商系统,团队使用 Caffeine 缓存存储商品信息。代码看起来很简单:

@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("productCache");
        cacheManager.setCaffeine(Caffeine.newBuilder());
        return cacheManager;
    }
}

@Service
public class ProductService {
    @Cacheable(value = "productCache", key = "#productId")
    public ProductDTO getProduct(Long productId) {
        // 从数据库获取商品
        return productRepository.findById(productId)
                .map(this::convertToDTO)
                .orElseThrow(() -> new ProductNotFoundException(productId));
    }
}

问题分析:看出问题了吗?我们没有设置缓存的最大容量和过期时间!随着商品数量增加,缓存不断膨胀,最终导致 JVM 内存不足。

┌─────────────────────────────────┐
│        JVM堆内存空间            │
│                                 │
│  ┌─────────────────────────┐    │
│  │     无限制缓存空间      │    │
│  │                         │    │
│  │   [商品1] [商品2] ...   │    │
│  │                         │    │
│  │  随着商品不断增加,     │    │
│  │  缓存占用空间不断扩大   │    │
│  └─────────────────────────┘    │
│                                 │
│  可用内存空间越来越小 ↓↓↓       │
│                                 │
└─────────────────────────────────┘

解决方案:设置最大缓存容量和过期策略

@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("productCache");
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(10000)  // 最多缓存10000个商品
                .expireAfterWrite(1, TimeUnit.HOURS)  // 缓存项写入1小时后过期
                .recordStats());  // 启用统计,便于监控
        return cacheManager;
    }
}

2. 缓存键设计不当:内存浪费和命中率低

案例:一个用户权限系统,缓存用户的权限信息:

@Service
public class AuthorizationService {
    @Cacheable(value = "permissionCache", key = "#request.toString()")
    public boolean hasPermission(PermissionRequest request) {
        // 复杂的权限检查逻辑
        return checkUserPermission(request);
    }
}

问题分析:使用整个 request 对象作为缓存键,每个微小的请求参数变化都会生成新的缓存项,导致缓存命中率极低,且占用大量内存。

解决方案:精心设计缓存键,只包含影响结果的关键属性

@Service
public class AuthorizationService {
    @Cacheable(value = "permissionCache",
              key = "#request.userId + '_' + #request.resourceType + '_' + #request.actionType")
    public boolean hasPermission(PermissionRequest request) {
        // 复杂的权限检查逻辑
        return checkUserPermission(request);
    }
}

优化改进:使用自定义 KeyGenerator 避免硬编码拼接

@Configuration
public class CacheConfig extends CachingConfigurerSupport {
    @Override
    public KeyGenerator keyGenerator() {
        return new CustomKeyGenerator();
    }
}

public class CustomKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        StringBuilder sb = new StringBuilder();
        // 添加类名和方法名
        sb.append(target.getClass().getSimpleName()).append(":")
          .append(method.getName());

        // 智能处理参数
        if (params.length > 0) {
            sb.append(":");
            for (Object param : params) {
                if (param == null) {
                    sb.append("null");
                } else if (param instanceof PermissionRequest) {
                    // 特殊处理PermissionRequest
                    PermissionRequest req = (PermissionRequest) param;
                    sb.append(req.getUserId())
                      .append("_").append(req.getResourceType())
                      .append("_").append(req.getActionType());
                } else {
                    sb.append(param.toString());
                }
                sb.append("_");
            }
        }

        return sb.toString();
    }
}

// 使用示例
@Service
public class AuthorizationService {
    // 使用自定义KeyGenerator
    @Cacheable(value = "permissionCache", keyGenerator = "keyGenerator")
    public boolean hasPermission(PermissionRequest request) {
        // 复杂的权限检查逻辑
        return checkUserPermission(request);
    }
}

3. 缓存粒度过大:内存浪费和更新频繁

案例:一个社交应用缓存用户信息,包括经常变化的状态数据:

@Service
public class UserProfileService {
    @Cacheable(value = "userCache", key = "#userId")
    public UserProfileDTO getUserProfile(Long userId) {
        UserEntity user = userRepository.findById(userId).orElseThrow();
        // 包含基本信息、状态、统计数据等
        return convertToDTO(user);
    }
}

问题分析:将频繁变化的数据(如在线状态)和相对稳定的数据(如用户基本信息)放在同一个缓存项中,导致整个缓存项频繁失效,缓存效果大打折扣。

┌───────────────────────────────────────────┐
│            单一大粒度缓存项               │
│                                           │
│  ┌─────────────┬─────────────┬─────────┐  │
│  │ 用户基本信息 │ 在线状态信息 │ 统计数据 │  │
│  │ (很少变化)   │ (频繁变化)   │ (偶尔变化)│  │
│  └─────────────┴─────────────┴─────────┘  │
│                                           │
│       任何部分变化都导致整体失效          │
└───────────────────────────────────────────┘

解决方案:按数据变化频率拆分缓存

@Service
public class UserProfileService {
    @Cacheable(value = "userBasicInfoCache", key = "#userId")
    public UserBasicInfoDTO getUserBasicInfo(Long userId) {
        // 获取基本信息(很少变化)
        return userRepository.findBasicInfoById(userId);
    }

    @Cacheable(value = "userStatusCache", key = "#userId", unless = "#result == null",
              condition = "#userId != null")
    public UserStatusDTO getUserStatus(Long userId) {
        // 获取状态信息(频繁变化),设置短期过期时间
        return statusRepository.findLatestByUserId(userId);
    }

    public UserProfileDTO getUserProfile(Long userId) {
        // 组合不同缓存数据
        UserBasicInfoDTO basicInfo = getUserBasicInfo(userId);
        UserStatusDTO status = getUserStatus(userId);
        UserStatsDTO stats = getUserStats(userId);
        return new UserProfileDTO(basicInfo, status, stats);
    }
}

二、高并发场景下的缓存灾难

1. 缓存穿透:恶意请求的噩梦

案例:一个商品查询 API,当请求不存在的商品 ID 时,缓存无法发挥作用:

@RestController
public class ProductController {
    @GetMapping("/products/{id}")
    public ProductDTO getProduct(@PathVariable Long id) {
        return productService.getProduct(id);
    }
}

@Service
public class ProductService {
    @Cacheable(value = "productCache", key = "#productId")
    public ProductDTO getProduct(Long productId) {
        // 如果是不存在的ID,每次都会查询数据库
        return productRepository.findById(productId)
                .map(this::convertToDTO)
                .orElse(null); // 返回null,不会被缓存
    }
}

问题分析:当大量请求查询不存在的 ID 时(例如恶意攻击),会导致大量请求直接打到数据库,绕过缓存,引发数据库压力剧增。

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  恶意请求   │ -> │    缓存     │ -> │   数据库    │
│ 不存在的ID  │    │  未命中!    │    │ 大量查询!   │
└─────────────┘    └─────────────┘    └─────────────┘
      │                  │                  │
      │                  │                  │
      v                  v                  v
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  更多请求   │ -> │  仍未命中!  │ -> │ 数据库压力  │
│ 不存在的ID  │    │             │    │ 持续增加!   │
└─────────────┘    └─────────────┘    └─────────────┘

解决方案:布隆过滤器 + 空值缓存

@Service
public class ProductService {
    private BloomFilter<Long> productIdFilter;

    @PostConstruct
    public void initBloomFilter() {
        // 初始化布隆过滤器,加载所有商品ID
        productIdFilter = BloomFilter.create(
            Funnels.longFunnel(),
            1000000,  // 预期元素数量
            0.01      // 误判率
        );

        // 分批加载所有商品ID到布隆过滤器,避免内存占用过高
        long lastId = 0;
        boolean hasMore = true;
        int batchSize = 10000;
        int totalLoaded = 0;

        while (hasMore) {
            List<Long> idBatch = productRepository.findIdsBatchAfter(lastId, batchSize);

            if (idBatch.isEmpty()) {
                hasMore = false;
            } else {
                for (Long id : idBatch) {
                    productIdFilter.put(id);
                    lastId = Math.max(lastId, id);
                }

                totalLoaded += idBatch.size();
                log.info("已加载{}个商品ID到布隆过滤器", totalLoaded);
            }
        }
    }

    public ProductDTO getProduct(Long productId) {
        // 布隆过滤器预判断,可能存在误判情况(判定不存在实际存在)
        if (!productIdFilter.mightContain(productId)) {
            log.debug("布隆过滤器判断商品ID{}可能不存在", productId);
            // 即使布隆过滤器判断不存在,仍然查询一次数据库确认
            // 避免因为布隆过滤器误判而漏查有效数据
        }

        // 查询缓存
        String cacheKey = "product:" + productId;
        ProductDTO product = (ProductDTO) cacheService.get(cacheKey);

        if (product != null) {
            // 注意: 这里需要区分空值占位符和有效数据
            if (product instanceof EmptyProductDTO) {
                return null; // 返回真正的null给调用者
            }
            return product;
        }

        // 查询数据库
        product = productRepository.findById(productId)
                .map(this::convertToDTO)
                .orElse(null);

        // 空值缓存处理 - Spring Cache的配置方式
        /*
           使用Spring的@Cacheable时需要注意:
           @Cacheable(value = "productCache", key = "#productId", unless = "#result == null")
           默认情况下Spring Cache不会缓存null值,需要通过unless = "#result == null"取反来允许缓存null

           或者使用自定义CacheManager:
           public CacheManager cacheManager() {
               return new CaffeineCacheManager() {
                   @Override
                   protected Cache createCache(String name) {
                       return new CaffeineCache(name, createNativeCaffeineCache(name), true); // allowNullValues=true
                   }
               };
           }
        */

        // 使用自定义cacheService时的空值处理
        if (product != null) {
            cacheService.put(cacheKey, product, 3600); // 有效值缓存1小时
        } else {
            // 存储空值占位符,而不是直接存null
            cacheService.put(cacheKey, new EmptyProductDTO(), 60); // 空值缓存1分钟
        }

        return product;
    }
}

// 空值占位对象
public class EmptyProductDTO extends ProductDTO {
    public EmptyProductDTO() {
        super(0L, "", "", 0.0);
    }
}

2. 缓存击穿:热点数据失效

案例:秒杀系统中的热门商品缓存失效

问题分析:当热点数据缓存恰好过期时,大量并发请求同时击穿缓存,直接请求数据库,导致数据库瞬间压力剧增。

解决方案:互斥锁(单应用)或分布式锁(集群环境)防止并发重建缓存

@Service
public class HotProductService {
    private final Map<String, ReentrantLock> keyLockMap = new ConcurrentHashMap<>();
    private static final int MAX_RETRY = 3; // 最大重试次数

    public ProductDTO getHotProduct(Long productId) {
        String cacheKey = "hotProduct:" + productId;
        ProductDTO product = null;
        int retryCount = 0;

        // 使用循环而非递归,避免栈溢出风险
        while (product == null && retryCount < MAX_RETRY) {
            product = (ProductDTO) cacheService.get(cacheKey);

            if (product != null) {
                return product;
            }

            // 获取该key的锁
            ReentrantLock lock = keyLockMap.computeIfAbsent(cacheKey, k -> new ReentrantLock());

            // 尝试获取锁,避免并发重建缓存
            boolean locked = lock.tryLock();
            try {
                if (locked) {
                    // 双重检查,避免其他线程已经重建缓存
                    product = (ProductDTO) cacheService.get(cacheKey);
                    if (product != null) {
                        return product;
                    }

                    // 从数据库获取数据并重建缓存
                    product = productRepository.findById(productId)
                            .map(this::convertToDTO)
                            .orElse(null);

                    if (product != null) {
                        cacheService.put(cacheKey, product, 3600); // 缓存1小时
                    }

                    // 即使product为null也跳出循环,避免无谓的重试
                    break;
                } else {
                    // 没获取到锁,说明有其他线程正在重建缓存
                    // 短暂等待后重试从缓存获取
                    Thread.sleep(50);
                    retryCount++; // 增加重试计数

                    // 在日志中记录重试情况,便于监控和排查问题
                    if (retryCount >= MAX_RETRY) {
                        log.warn("获取热点商品{}缓存重试次数达到上限", productId);
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            } finally {
                if (locked) {
                    lock.unlock();
                    // 清理不再使用的锁对象
                    keyLockMap.remove(cacheKey);
                }
            }
        }

        return product;
    }
}

// 集群环境下使用Redisson实现分布式锁
@Service
public class DistributedHotProductService {
    @Autowired
    private RedissonClient redissonClient;

    public ProductDTO getHotProduct(Long productId) {
        String cacheKey = "hotProduct:" + productId;
        ProductDTO product = (ProductDTO) cacheService.get(cacheKey);

        if (product != null) {
            return product;
        }

        // 创建分布式锁
        String lockKey = "lock:hotProduct:" + productId;
        RLock lock = redissonClient.getLock(lockKey);

        boolean locked = false;
        try {
            // 尝试获取锁,最多等待100ms,锁过期时间5秒
            locked = lock.tryLock(100, 5000, TimeUnit.MILLISECONDS);

            if (locked) {
                // 双重检查,避免其他服务实例已经重建缓存
                product = (ProductDTO) cacheService.get(cacheKey);
                if (product != null) {
                    return product;
                }

                // 从数据库获取数据并重建缓存
                product = productRepository.findById(productId)
                        .map(this::convertToDTO)
                        .orElse(null);

                if (product != null) {
                    cacheService.put(cacheKey, product, 3600);
                }
            } else {
                log.debug("未能获取商品{}的分布式锁,等待其他实例重建缓存", productId);

                // 短暂等待后再查一次缓存
                Thread.sleep(200);
                product = (ProductDTO) cacheService.get(cacheKey);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (locked) {
                lock.unlock();
            }
        }

        return product;
    }
}

3. 缓存雪崩:系统的噩梦

案例:某电商平台在促销活动期间,大量缓存同时过期

问题分析:如果大量缓存项设置了相同的过期时间,会导致它们同时失效,引发缓存雪崩,系统性能急剧下降。

┌─────────────────────────────────────────────────────┐
│                    时间轴                           │
│                                                     │
│  ┌─────┐  ┌─────┐  ┌─────┐       ┌─────┐  ┌─────┐  │
│  │缓存1│  │缓存2│  │缓存3│  ...  │缓存n-1│ │缓存n│  │
│  └─────┘  └─────┘  └─────┘       └─────┘  └─────┘  │
│     │        │        │             │        │     │
│     │        │        │             │        │     │
│     ▼        ▼        ▼             ▼        ▼     │
│  ┌─────────────────────────────────────────────┐   │
│  │              同一时间点过期                 │   │
│  └─────────────────────────────────────────────┘   │
│                        │                           │
│                        ▼                           │
│  ┌─────────────────────────────────────────────┐   │
│  │        大量请求直接访问数据库               │   │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

解决方案

  1. 随机过期时间
  2. 多级缓存架构
  3. 熔断降级机制
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();

        // 配置不同缓存空间的策略
        Map<String, Caffeine<Object, Object>> cacheBuilders = new HashMap<>();

        // 商品缓存 - 随机过期时间
        cacheBuilders.put("productCache", Caffeine.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(randomExpireTime(3600, 4000), TimeUnit.SECONDS)); // 1小时左右随机过期

        // 用户缓存 - 随机过期时间
        cacheBuilders.put("userCache", Caffeine.newBuilder()
                .maximumSize(5000)
                .expireAfterWrite(randomExpireTime(1800, 2200), TimeUnit.SECONDS)); // 30分钟左右随机过期

        cacheManager.setCacheBuildersByName(cacheBuilders);
        return cacheManager;
    }

    // 生成随机过期时间,避免同时过期
    private int randomExpireTime(int baseSeconds, int maxSeconds) {
        return baseSeconds + new Random().nextInt(maxSeconds - baseSeconds);
    }
}

// 多级缓存架构实现
@Service
public class MultiLevelCacheService {
    // 一级缓存:JVM本地缓存 (极快,但容量有限)
    private final Cache<String, Object> localCache;

    // 二级缓存:应用级缓存 (较快,容量较大)
    private final CaffeineCacheManager caffeineCacheManager;

    // 三级缓存:分布式缓存 (慢于本地,但可跨实例共享)
    private final RedisTemplate<String, Object> redisTemplate;

    public MultiLevelCacheService() {
        // 本地缓存配置:容量小,超短过期时间,适合极热点数据
        this.localCache = CacheBuilder.newBuilder()
                .maximumSize(1000) // 仅缓存1000个对象
                .expireAfterWrite(30, TimeUnit.SECONDS) // 30秒过期
                .recordStats() // 启用统计
                .build();

        // 应用级缓存:容量适中,短过期时间
        this.caffeineCacheManager = new CaffeineCacheManager();
        caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(50000) // 缓存50000个对象
                .expireAfterWrite(randomExpireTime(300, 600), TimeUnit.SECONDS) // 5-10分钟随机过期
                .recordStats());

        // Redis缓存配置在Bean中初始化
    }

    public Object get(String key, String cacheRegion) {
        String fullKey = cacheRegion + ":" + key;

        // 1. 查询本地缓存
        Object value = localCache.getIfPresent(fullKey);
        if (value != null) {
            log.debug("本地缓存命中: {}", fullKey);
            return value;
        }

        // 2. 查询Caffeine缓存
        Cache caffeineCache = caffeineCacheManager.getCache(cacheRegion);
        if (caffeineCache != null) {
            Cache.ValueWrapper wrapper = caffeineCache.get(key);
            if (wrapper != null) {
                value = wrapper.get();
                if (value != null) {
                    log.debug("Caffeine缓存命中: {}", fullKey);
                    // 回填本地缓存
                    localCache.put(fullKey, value);
                    return value;
                }
            }
        }

        // 3. 查询Redis缓存
        try {
            value = redisTemplate.opsForValue().get(fullKey);
            if (value != null) {
                log.debug("Redis缓存命中: {}", fullKey);
                // 回填本地和Caffeine缓存
                localCache.put(fullKey, value);
                if (caffeineCache != null) {
                    caffeineCache.put(key, value);
                }
                return value;
            }
        } catch (Exception e) {
            log.warn("Redis缓存访问异常: {}", e.getMessage());
            // Redis异常不影响后续逻辑
        }

        return null; // 所有级别缓存都未命中
    }

    // 更新缓存的方法会分别更新三级缓存,并使用随机过期时间
    // ...
}

// 熔断降级机制 - 详细配置说明
@Service
public class ResilientCacheService {
    // 使用Resilience4j熔断器
    private final CircuitBreaker circuitBreaker;

    public ResilientCacheService() {
        // 熔断器配置详解
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
                .failureRateThreshold(50) // 当失败率达到50%时触发熔断
                .waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断后等待30秒再尝试半开状态
                .permittedNumberOfCallsInHalfOpenState(10) // 半开状态允许10次调用测试
                .minimumNumberOfCalls(20) // 至少20次调用才开始计算失败率
                .slidingWindowSize(100) // 基于最近100次调用计算失败率
                .slidingWindowType(SlidingWindowType.COUNT_BASED) // 基于调用次数的滑动窗口
                .recordExceptions(TimeoutException.class, IOException.class) // 记录哪些异常算作失败
                .build();

        circuitBreaker = CircuitBreaker.of("cacheCircuitBreaker", config);

        // 添加状态变化监听器,便于监控
        circuitBreaker.getEventPublisher()
            .onStateTransition(event -> {
                CircuitBreaker.State newState = event.getStateTransition().getToState();
                log.warn("缓存熔断器状态变化: {} -> {}",
                         event.getStateTransition().getFromState(),
                         newState);

                if (newState == CircuitBreaker.State.OPEN) {
                    // 发送告警通知
                    notificationService.sendAlert("缓存服务熔断,请检查缓存服务状态!");
                }
            });
    }

    public ProductDTO getProduct(Long productId) {
        // 使用熔断器包装缓存调用
        Supplier<ProductDTO> cacheSupplier = CircuitBreaker.decorateSupplier(
                circuitBreaker, () -> getCachedProduct(productId));

        try {
            return cacheSupplier.get();
        } catch (Exception e) {
            log.warn("缓存服务异常,启动降级: {}", e.getMessage());
            // 降级处理,例如返回基础数据或默认值
            return getFallbackProduct(productId);
        }
    }

    private ProductDTO getCachedProduct(Long productId) {
        // 正常的缓存获取逻辑
        // ...
    }

    private ProductDTO getFallbackProduct(Long productId) {
        // 降级逻辑,从只读数据库或本地快照获取基础数据
        try {
            return emergencyDataService.getProductBasicInfo(productId);
        } catch (Exception e) {
            log.error("降级服务也失败,返回默认空对象: {}", e.getMessage());
            // 最终降级:返回默认对象
            return new ProductDTO(productId, "临时商品信息", "系统繁忙,显示默认信息", 0.0);
        }
    }
}

三、缓存与数据库的一致性问题

1. 缓存更新策略不当

案例:用户信息更新后,缓存未及时刷新导致数据不一致

@Service
public class UserService {
    @Cacheable(value = "userCache", key = "#userId")
    public UserDTO getUser(Long userId) {
        return userRepository.findById(userId)
                .map(this::convertToDTO)
                .orElse(null);
    }

    // 更新用户信息,但忘记更新缓存
    public void updateUser(Long userId, UserUpdateRequest request) {
        UserEntity user = userRepository.findById(userId).orElseThrow();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        userRepository.save(user);
        // 缓存中的数据未更新!
    }
}

问题分析:数据库更新后,缓存未同步更新,导致数据不一致。用户看到的是旧数据,造成困惑和业务错误。

解决方案:Cache-Aside Pattern(旁路缓存模式)

@Service
public class UserService {
    @Cacheable(value = "userCache", key = "#userId", unless = "#result == null")
    public UserDTO getUser(Long userId) {
        return userRepository.findById(userId)
                .map(this::convertToDTO)
                .orElse(null);
    }

    // 正确的更新方式:先更新数据库,再删除缓存
    @Transactional
    public void updateUser(Long userId, UserUpdateRequest request) {
        UserEntity user = userRepository.findById(userId).orElseThrow();
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        userRepository.save(user);

        // 更新后删除缓存,而不是更新缓存
        cacheManager.getCache("userCache").evict(userId);
    }
}

2. 分布式环境下的缓存一致性

问题分析:在分布式系统中,多个服务实例可能同时操作缓存和数据库,导致复杂的一致性问题。

解决方案

  1. 使用事务性消息确保缓存同步
  2. 采用最终一致性策略
@Service
public class DistributedUserService {
    @Autowired
    private KafkaTemplate<String, CacheInvalidationEvent> kafkaTemplate;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    private CacheInvalidationMessageRepository messageRepository;

    public void updateUser(Long userId, UserUpdateRequest request) {
        // 使用编程式事务确保数据库更新和消息发送的原子性
        transactionTemplate.execute(status -> {
            try {
                // 1. 更新数据库
                UserEntity user = userRepository.findById(userId).orElseThrow();
                user.setName(request.getName());
                user.setEmail(request.getEmail());
                userRepository.save(user);

                // 2. 将缓存失效消息保存到消息表(与业务在同一事务)
                CacheInvalidationMessage message = new CacheInvalidationMessage();
                message.setCacheName("userCache");
                message.setCacheKey(userId.toString());
                message.setStatus("PENDING");
                message.setCreatedTime(new Date());
                messageRepository.save(message);

                return true;
            } catch (Exception e) {
                // 事务回滚
                status.setRollbackOnly();
                log.error("用户更新失败: {}", e.getMessage());
                throw e;
            }
        });

        // 3. 事务提交成功后,异步发送消息到Kafka
        // 这是一个补偿措施:即使消息发送失败,定时任务也会重试发送PENDING状态的消息
        sendPendingCacheInvalidationMessages();
    }

    // 定时任务:每分钟扫描一次未发送的消息并重试
    @Scheduled(fixedRate = 60000)
    public void sendPendingCacheInvalidationMessages() {
        List<CacheInvalidationMessage> pendingMessages =
            messageRepository.findByStatusAndCreatedTimeBefore("PENDING",
                new Date(System.currentTimeMillis() - 300000)); // 5分钟前的消息

        for (CacheInvalidationMessage message : pendingMessages) {
            try {
                // 发送到Kafka
                CacheInvalidationEvent event = new CacheInvalidationEvent(
                    message.getCacheName(), message.getCacheKey());

                kafkaTemplate.send("cache-invalidation", event).get(); // 同步等待发送结果

                // 更新消息状态为已发送
                message.setStatus("SENT");
                message.setSentTime(new Date());
                messageRepository.save(message);

                log.info("成功发送缓存失效消息: {}:{}", message.getCacheName(), message.getCacheKey());
            } catch (Exception e) {
                log.error("发送缓存失效消息失败,将在下次重试: {}", e.getMessage());
                // 失败时不更新状态,下次还会重试

                // 如果消息重试次数过多,可以考虑标记为失败或报警
                if (message.getRetryCount() > 10) {
                    message.setStatus("FAILED");
                    messageRepository.save(message);

                    // 发送告警
                    notificationService.sendAlert("缓存同步消息发送失败超过重试上限,请检查!");
                } else {
                    // 增加重试计数
                    message.setRetryCount(message.getRetryCount() + 1);
                    messageRepository.save(message);
                }
            }
        }
    }

    // 在所有服务实例中监听缓存失效消息
    @KafkaListener(topics = "cache-invalidation")
    public void handleCacheInvalidation(CacheInvalidationEvent event) {
        try {
            if ("userCache".equals(event.getCacheName())) {
                log.info("收到缓存失效消息,清除缓存: {}:{}",
                        event.getCacheName(), event.getKey());
                cacheManager.getCache(event.getCacheName()).evict(event.getKey());
            }
        } catch (Exception e) {
            log.error("处理缓存失效消息失败: {}", e.getMessage());
            // 考虑是否需要重试或者其他补偿措施
        }
    }
}

四、缓存监控与优化

1. 缓存效率监控

案例:一个团队发现系统性能不佳,但无法确定是否与缓存有关

解决方案:实现缓存监控和统计

@Configuration
public class CacheMonitoringConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(1, TimeUnit.HOURS)
                .recordStats()); // 启用统计
        return cacheManager;
    }

    @Bean
    public CacheMetricsCollector cacheMetricsCollector(CacheManager cacheManager) {
        return new CacheMetricsCollector(cacheManager);
    }
}

@Component
public class CacheMetricsCollector {
    private final CacheManager cacheManager;
    private final MeterRegistry meterRegistry;

    @Scheduled(fixedRate = 60000) // 每分钟收集一次
    public void collectMetrics() {
        for (String cacheName : cacheManager.getCacheNames()) {
            Cache cache = cacheManager.getCache(cacheName);
            if (cache instanceof CaffeineCache) {
                CaffeineCache caffeineCache = (CaffeineCache) cache;
                com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache = caffeineCache.getNativeCache();

                CacheStats stats = nativeCache.stats();

                // 记录各种指标
                meterRegistry.gauge("cache.size", nativeCache.estimatedSize());
                meterRegistry.gauge("cache.hit.ratio", stats.hitRate());
                meterRegistry.gauge("cache.miss.ratio", stats.missRate());
                meterRegistry.gauge("cache.eviction.count", stats.evictionCount());

                log.info("Cache stats for {}: hit ratio={}, miss ratio={}, size={}",
                        cacheName, stats.hitRate(), stats.missRate(), nativeCache.estimatedSize());
            }
        }
    }
}

2. 缓存预热策略

案例:系统重启后,缓存为空,导致短时间内大量请求直接访问数据库

解决方案:实现缓存预热

@Configuration
public class AsyncConfig {
    @Bean(name = "cacheWarmerExecutor")
    public Executor cacheWarmerExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("CacheWarmer-");
        executor.initialize();
        return executor;
    }
}

@Component
public class CacheWarmer {
    @Autowired
    private ProductService productService;

    @Autowired
    private UserService userService;

    private final AtomicInteger warmedUpItems = new AtomicInteger(0);

    // 系统启动后执行
    @EventListener(ApplicationReadyEvent.class)
    public void warmUpCaches() {
        log.info("开始预热缓存...");

        // 预热热门商品缓存
        List<Long> hotProductIds = productService.getHotProductIds();
        log.info("计划预热{}个热门商品...", hotProductIds.size());

        // 使用专用线程池而非parallelStream,避免占用公共ForkJoinPool
        for (Long productId : hotProductIds) {
            warmupProductAsync(productId);
        }

        // 预热活跃用户缓存
        List<Long> activeUserIds = userService.getActiveUserIds();
        log.info("计划预热{}个活跃用户...", activeUserIds.size());

        for (Long userId : activeUserIds) {
            warmupUserAsync(userId);
        }
    }

    @Async("cacheWarmerExecutor")
    public void warmupProductAsync(Long productId) {
        try {
            productService.getProduct(productId);
            int count = warmedUpItems.incrementAndGet();

            if (count % 100 == 0) {
                log.info("已预热{}个缓存项", count);
            }
        } catch (Exception e) {
            log.warn("预热商品{}缓存失败: {}", productId, e.getMessage());
        }
    }

    @Async("cacheWarmerExecutor")
    public void warmupUserAsync(Long userId) {
        try {
            userService.getUser(userId);
            int count = warmedUpItems.incrementAndGet();

            if (count % 100 == 0) {
                log.info("已预热{}个缓存项", count);
            }
        } catch (Exception e) {
            log.warn("预热用户{}缓存失败: {}", userId, e.getMessage());
        }
    }

    // 可以添加一个监控预热进度的接口,便于在启动时查看
    public int getWarmedUpItemCount() {
        return warmedUpItems.get();
    }
}

五、实战策略与落地经验总结

  1. 设置合理的缓存容量和过期策略

    • 为每种缓存设置最大容量限制
    • 根据数据更新频率设置过期时间
    • 为不同类型的数据使用不同的过期策略
  2. 精心设计缓存键

    • 只包含影响结果的关键属性
    • 避免使用完整对象或过长字符串作为键
    • 考虑使用键前缀区分不同类型的缓存
    • 使用自定义 KeyGenerator 提升可维护性
  3. 合理的缓存粒度

    • 按照数据变化频率拆分缓存
    • 热点数据可以单独缓存
    • 避免缓存过大的对象
  4. 防御性缓存设计

    • 实现布隆过滤器防止缓存穿透,注意分批加载 ID 避免内存压力
    • 使用互斥锁(单应用)或 Redisson 分布式锁(集群环境)防止缓存击穿
    • 设置随机过期时间防止缓存雪崩
    • 实现熔断降级机制应对极端情况
  5. 正确的缓存更新策略

    • 优先使用 Cache-Aside 模式
    • 先更新数据库,再删除缓存
    • 在分布式环境使用事务性消息确保缓存同步
  6. 全面的缓存监控

    • 收集命中率、大小等关键指标
    • 设置告警阈值
    • 定期分析缓存效率
  7. 缓存预热

    • 系统启动时预热热点数据
    • 大促活动前预热相关数据
    • 使用专用线程池进行异步预热,避免影响系统启动

结语

缓存是一把双刃剑,用得好可以大幅提升系统性能,用不好则会带来内存泄漏和数据不一致等问题。希望本文的案例和解决方案能帮助你在 SpringBoot 项目中正确使用缓存,避免各种潜在的陷阱!


感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!

如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

异常君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值