Skip to content

缓存

LCGYL Framework 提供统一的缓存抽象,支持多种缓存实现,帮助你提升应用性能。

缓存配置

基本配置

properties
# application.properties
cache.enabled=true
cache.type=memory
cache.default-ttl=3600
cache.max-size=10000

内存缓存

properties
cache.type=memory
cache.memory.initial-capacity=100
cache.memory.maximum-size=10000
cache.memory.expire-after-write=3600
cache.memory.expire-after-access=1800

Redis 缓存

properties
cache.type=redis
cache.redis.host=localhost
cache.redis.port=6379
cache.redis.password=secret
cache.redis.database=0
cache.redis.timeout=3000
cache.redis.pool.max-active=8
cache.redis.pool.max-idle=8
cache.redis.pool.min-idle=0

缓存注解

@Cacheable

缓存方法返回值。

java
@Component
public class UserService {
    
    @Inject
    private UserRepository userRepository;
    
    @Cacheable(value = "users", key = "#id")
    public User findById(Long id) {
        // 第一次调用会执行,结果被缓存
        // 后续调用直接返回缓存
        return userRepository.findById(id);
    }
    
    @Cacheable(value = "users", key = "#name")
    public List<User> findByName(String name) {
        return userRepository.findByName(name);
    }
    
    @Cacheable(value = "users", key = "'all'")
    public List<User> findAll() {
        return userRepository.findAll();
    }
}

@CachePut

更新缓存。

java
@Component
public class UserService {
    
    @CachePut(value = "users", key = "#user.id")
    public User save(User user) {
        // 总是执行方法,并更新缓存
        userRepository.save(user);
        return user;
    }
    
    @CachePut(value = "users", key = "#result.id")
    public User create(String name, String email) {
        User user = new User(name, email);
        userRepository.save(user);
        return user;
    }
}

@CacheEvict

删除缓存。

java
@Component
public class UserService {
    
    @CacheEvict(value = "users", key = "#id")
    public void delete(Long id) {
        userRepository.delete(id);
    }
    
    @CacheEvict(value = "users", allEntries = true)
    public void clearCache() {
        // 清除 users 缓存的所有条目
    }
    
    @CacheEvict(value = "users", key = "#user.id", beforeInvocation = true)
    public void update(User user) {
        // 在方法执行前删除缓存
        userRepository.save(user);
    }
}

@Caching

组合多个缓存操作。

java
@Component
public class UserService {
    
    @Caching(
        put = {
            @CachePut(value = "users", key = "#user.id"),
            @CachePut(value = "usersByEmail", key = "#user.email")
        },
        evict = {
            @CacheEvict(value = "userList", allEntries = true)
        }
    )
    public User save(User user) {
        userRepository.save(user);
        return user;
    }
}

缓存键

SpEL 表达式

java
// 使用参数
@Cacheable(value = "users", key = "#id")
public User findById(Long id) { }

// 使用多个参数
@Cacheable(value = "users", key = "#name + ':' + #status")
public List<User> findByNameAndStatus(String name, UserStatus status) { }

// 使用对象属性
@Cacheable(value = "orders", key = "#order.userId + ':' + #order.status")
public List<Order> findOrders(OrderQuery order) { }

// 使用方法返回值
@CachePut(value = "users", key = "#result.id")
public User create(String name) { }

// 使用条件
@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public User findById(Long id) { }

// 使用 unless
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) { }

自定义键生成器

java
@Component
public class CustomKeyGenerator implements KeyGenerator {
    
    @Override
    public Object generate(Object target, Method method, Object... params) {
        return target.getClass().getSimpleName() + ":" +
               method.getName() + ":" +
               Arrays.stream(params)
                   .map(Object::toString)
                   .collect(Collectors.joining(":"));
    }
}

// 使用
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User findById(Long id) { }

缓存管理器

配置缓存管理器

java
@Component
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(
            new ConcurrentMapCache("users"),
            new ConcurrentMapCache("orders"),
            new ConcurrentMapCache("products")
        ));
        return cacheManager;
    }
}

自定义缓存配置

java
@Component
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCacheSpecification("maximumSize=1000,expireAfterWrite=3600s");
        return cacheManager;
    }
    
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .withCacheConfiguration("users", 
                config.entryTtl(Duration.ofMinutes(30)))
            .withCacheConfiguration("orders", 
                config.entryTtl(Duration.ofMinutes(10)))
            .build();
    }
}

编程式缓存

使用 CacheService

java
@Component
public class UserService {
    
    @Inject
    private CacheService cacheService;
    
    @Inject
    private UserRepository userRepository;
    
    public User findById(Long id) {
        String key = "user:" + id;
        
        // 尝试从缓存获取
        User user = cacheService.get(key, User.class);
        if (user != null) {
            return user;
        }
        
        // 从数据库查询
        user = userRepository.findById(id);
        
        // 存入缓存
        if (user != null) {
            cacheService.put(key, user, Duration.ofHours(1));
        }
        
        return user;
    }
    
    public void delete(Long id) {
        userRepository.delete(id);
        cacheService.evict("user:" + id);
    }
    
    public void clearUserCache() {
        cacheService.evictByPrefix("user:");
    }
}

使用 Cache 接口

java
@Component
public class UserService {
    
    @Inject
    private CacheManager cacheManager;
    
    public User findById(Long id) {
        Cache cache = cacheManager.getCache("users");
        
        // 获取缓存
        Cache.ValueWrapper wrapper = cache.get(id);
        if (wrapper != null) {
            return (User) wrapper.get();
        }
        
        // 查询并缓存
        User user = userRepository.findById(id);
        if (user != null) {
            cache.put(id, user);
        }
        
        return user;
    }
    
    // 使用 get 方法的回调
    public User findByIdWithCallback(Long id) {
        Cache cache = cacheManager.getCache("users");
        return cache.get(id, () -> userRepository.findById(id));
    }
}

多级缓存

本地 + 远程缓存

java
@Component
public class MultiLevelCacheService {
    
    private final Cache<String, Object> localCache;
    private final RedisTemplate<String, Object> redisTemplate;
    
    public MultiLevelCacheService() {
        this.localCache = Caffeine.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build();
    }
    
    public <T> T get(String key, Class<T> type) {
        // 1. 先查本地缓存
        T value = (T) localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 2. 查 Redis
        value = (T) redisTemplate.opsForValue().get(key);
        if (value != null) {
            // 回填本地缓存
            localCache.put(key, value);
            return value;
        }
        
        return null;
    }
    
    public <T> void put(String key, T value, Duration ttl) {
        // 同时写入两级缓存
        localCache.put(key, value);
        redisTemplate.opsForValue().set(key, value, ttl);
    }
    
    public void evict(String key) {
        localCache.invalidate(key);
        redisTemplate.delete(key);
    }
}

缓存预热

启动时预热

java
@Component
public class CacheWarmer {
    
    @Inject
    private UserService userService;
    
    @Inject
    private ProductService productService;
    
    @PostConstruct
    public void warmUp() {
        logger.info("开始缓存预热...");
        
        // 预热热门用户
        List<Long> hotUserIds = getHotUserIds();
        hotUserIds.forEach(userService::findById);
        
        // 预热热门商品
        List<Long> hotProductIds = getHotProductIds();
        hotProductIds.forEach(productService::findById);
        
        logger.info("缓存预热完成");
    }
}

定时刷新

java
@Component
public class CacheRefresher {
    
    @Inject
    private CacheManager cacheManager;
    
    @Inject
    private UserRepository userRepository;
    
    @Scheduled(fixedRate = 300000)  // 每 5 分钟
    public void refreshHotData() {
        Cache cache = cacheManager.getCache("users");
        
        // 刷新热门数据
        List<User> hotUsers = userRepository.findHotUsers();
        hotUsers.forEach(user -> cache.put(user.getId(), user));
    }
}

缓存穿透防护

缓存空值

java
@Component
public class UserService {
    
    private static final Object NULL_VALUE = new Object();
    
    @Inject
    private CacheService cacheService;
    
    public User findById(Long id) {
        String key = "user:" + id;
        
        Object cached = cacheService.get(key);
        if (cached == NULL_VALUE) {
            return null;  // 缓存的空值
        }
        if (cached != null) {
            return (User) cached;
        }
        
        User user = userRepository.findById(id);
        if (user == null) {
            // 缓存空值,防止穿透
            cacheService.put(key, NULL_VALUE, Duration.ofMinutes(5));
        } else {
            cacheService.put(key, user, Duration.ofHours(1));
        }
        
        return user;
    }
}

布隆过滤器

java
@Component
public class BloomFilterService {
    
    private final BloomFilter<Long> userIdFilter;
    
    public BloomFilterService() {
        this.userIdFilter = BloomFilter.create(
            Funnels.longFunnel(),
            1000000,  // 预期元素数量
            0.01      // 误判率
        );
    }
    
    @PostConstruct
    public void init() {
        // 初始化布隆过滤器
        List<Long> allUserIds = userRepository.findAllIds();
        allUserIds.forEach(userIdFilter::put);
    }
    
    public boolean mightExist(Long userId) {
        return userIdFilter.mightContain(userId);
    }
    
    public void add(Long userId) {
        userIdFilter.put(userId);
    }
}

@Component
public class UserService {
    
    @Inject
    private BloomFilterService bloomFilter;
    
    public User findById(Long id) {
        // 先检查布隆过滤器
        if (!bloomFilter.mightExist(id)) {
            return null;  // 一定不存在
        }
        
        // 查缓存和数据库
        return findByIdWithCache(id);
    }
}

缓存击穿防护

互斥锁

java
@Component
public class UserService {
    
    @Inject
    private CacheService cacheService;
    
    @Inject
    private LockService lockService;
    
    public User findById(Long id) {
        String key = "user:" + id;
        
        // 查缓存
        User user = cacheService.get(key, User.class);
        if (user != null) {
            return user;
        }
        
        // 获取锁
        String lockKey = "lock:user:" + id;
        if (lockService.tryLock(lockKey, Duration.ofSeconds(10))) {
            try {
                // 双重检查
                user = cacheService.get(key, User.class);
                if (user != null) {
                    return user;
                }
                
                // 查数据库
                user = userRepository.findById(id);
                if (user != null) {
                    cacheService.put(key, user, Duration.ofHours(1));
                }
                return user;
            } finally {
                lockService.unlock(lockKey);
            }
        } else {
            // 等待并重试
            Thread.sleep(100);
            return findById(id);
        }
    }
}

逻辑过期

java
@Component
public class UserService {
    
    public User findById(Long id) {
        String key = "user:" + id;
        
        CacheEntry<User> entry = cacheService.getEntry(key);
        if (entry == null) {
            return loadAndCache(id);
        }
        
        // 检查逻辑过期
        if (entry.isExpired()) {
            // 异步刷新
            CompletableFuture.runAsync(() -> loadAndCache(id));
        }
        
        // 返回旧数据
        return entry.getValue();
    }
    
    private User loadAndCache(Long id) {
        User user = userRepository.findById(id);
        if (user != null) {
            cacheService.put("user:" + id, user, Duration.ofHours(1));
        }
        return user;
    }
}

最佳实践

1. 选择合适的缓存键

java
// ✅ 推荐:有意义的键
@Cacheable(value = "users", key = "'user:' + #id")

// ❌ 不推荐:无意义的键
@Cacheable(value = "users", key = "#id")

2. 设置合理的过期时间

java
// ✅ 推荐:根据数据特性设置
@Cacheable(value = "users", key = "#id")  // 用户数据,1小时
@Cacheable(value = "config", key = "#key")  // 配置数据,24小时
@Cacheable(value = "hotData", key = "#id")  // 热点数据,5分钟

3. 处理缓存失效

java
// ✅ 推荐:更新时同步清除缓存
@CacheEvict(value = "users", key = "#user.id")
public void update(User user) {
    userRepository.save(user);
}

4. 避免缓存大对象

java
// ✅ 推荐:只缓存必要的数据
@Cacheable(value = "userSummary", key = "#id")
public UserSummary getUserSummary(Long id) {
    return new UserSummary(user.getId(), user.getName());
}

// ❌ 不推荐:缓存大对象
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
    return userRepository.findByIdWithAllRelations(id);
}

常见问题

Q: 缓存和数据库不一致怎么办?

A:

  1. 使用 Cache-Aside 模式
  2. 更新数据时先删除缓存
  3. 设置合理的过期时间
  4. 使用消息队列同步

Q: 如何监控缓存命中率?

A:

java
@Component
public class CacheMetrics {
    
    @Inject
    private CacheManager cacheManager;
    
    public CacheStats getStats(String cacheName) {
        Cache cache = cacheManager.getCache(cacheName);
        // 返回命中率、大小等统计信息
    }
}

Q: 缓存雪崩怎么处理?

A:

  1. 设置随机过期时间
  2. 使用多级缓存
  3. 限流降级
  4. 缓存预热

下一步

Released under the Apache License 2.0