4-4 缓存集成

3 阅读4分钟

4-4 缓存集成

概念解析

Spring Cache 注解

注解说明
@EnableCaching开启缓存支持
@Cacheable方法结果放入缓存
@CachePut更新缓存(不影响方法执行)
@CacheEvict清除缓存
@Caching组合多个缓存操作

缓存策略

策略说明使用场景
ALL缓存所有数据数据量小、变化少
Cache-aside先查缓存,后查库大部分场景
Read-through缓存负责加载数据预热场景
Write-through同步写缓存和库数据一致性要求高
Write-behind异步批量写库高并发写场景

代码示例

1. Redis 配置

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // JSON 序列化
        Jackson2JsonRedisSerializer<Object> serializer =
            new Jackson2JsonRedisSerializer<>(Object.class);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // 配置缓存管理器
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))  // 默认过期时间
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));

        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withCacheConfiguration("users",
                config.entryTtl(Duration.ofMinutes(30)))
            .withCacheConfiguration("products",
                config.entryTtl(Duration.ofHours(2)))
            .build();
    }
}

2. 基本缓存使用

@Service
@CacheConfig(cacheNames = "users")  // 类级别统一配置
public class UserService {

    @Cacheable(key = "#id", unless = "#result == null")
    public User getUser(Long id) {
        log.info("查询数据库: {}", id);
        return userRepository.findById(id).orElse(null);
    }

    @CachePut(key = "#result.id")
    public User updateUser(User user) {
        return userRepository.save(user);
    }

    @CacheEvict(key = "#id")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }

    @CacheEvict(allEntries = true)
    public void refreshAll() {
        // 清除所有用户缓存
    }
}

3. 复杂缓存策略

@Service
public class ProductService {

    // 条件缓存
    @Cacheable(
        key = "#id",
        condition = "#id > 100",  // id > 100 才缓存
        unless = "#result.soldOut == true"  // 已售罄不缓存
    )
    public Product getProduct(Long id) {
        return productRepository.findById(id).orElse(null);
    }

    // 多级缓存
    @Cacheable(
        value = {"localCache", "redisCache"},
        key = "#key",
        cacheManager = "cacheManager"
    )
    public Object getWithMultiCache(String key) {
        return databaseService.find(key);
    }

    // 组合缓存操作
    @Caching(
        evict = {
            @CacheEvict(key = "#result.id"),
            @CacheEvict(value = "user-products", key = "#result.userId")
        }
    )
    public Product updateProduct(Product product) {
        return productRepository.save(product);
    }
}

4. Redis 缓存实现

@Service
public class RedisCacheService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String CACHE_PREFIX = "cache:";

    public <T> T get(String key, Class<T> type) {
        String cacheKey = CACHE_PREFIX + key;
        Object value = redisTemplate.opsForValue().get(cacheKey);
        return value != null ? type.cast(value) : null;
    }

    public void set(String key, Object value, long seconds) {
        String cacheKey = CACHE_PREFIX + key;
        redisTemplate.opsForValue().set(cacheKey, value,
            Duration.ofSeconds(seconds));
    }

    public void delete(String key) {
        redisTemplate.delete(CACHE_PREFIX + key);
    }

    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(CACHE_PREFIX + key);
    }

    public void expire(String key, long seconds) {
        redisTemplate.expire(CACHE_PREFIX + key, Duration.ofSeconds(seconds));
    }
}

5. 分布式锁

@Service
public class DistributedLockService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String LOCK_PREFIX = "lock:";

    public boolean tryLock(String key, long expireSeconds) {
        String lockKey = LOCK_PREFIX + key;
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(expireSeconds));
        return Boolean.TRUE.equals(result);
    }

    public void unlock(String key) {
        String lockKey = LOCK_PREFIX + key;
        redisTemplate.delete(lockKey);
    }

    public <T> T executeWithLock(String key, long timeout,
            Supplier<T> supplier) {
        if (tryLock(key, timeout)) {
            try {
                return supplier.get();
            } finally {
                unlock(key);
            }
        }
        throw new RuntimeException("获取锁失败");
    }
}

// 使用
@Service
public class StockService {

    @Autowired
    private DistributedLockService lockService;

    public void decreaseStock(Long productId, int quantity) {
        String lockKey = "stock:" + productId;
        lockService.executeWithLock(lockKey, 10, () -> {
            Stock stock = stockRepository.findByProductId(productId);
            stock.setQuantity(stock.getQuantity() - quantity);
            stockRepository.save(stock);
            return null;
        });
    }
}

6. 缓存雪崩/击穿处理

@Service
public class CacheStrategyService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 缓存雪崩:过期时间加随机值
    public void setWithRandomExpire(String key, Object value,
            long baseSeconds) {
        long actualSeconds = baseSeconds + new Random().nextInt(300);
        redisTemplate.opsForValue().set(key, value,
            Duration.ofSeconds(actualSeconds));
    }

    // 缓存击穿:分布式锁
    public Product getProductWithLock(Long id) {
        String cacheKey = "product:" + id;
        Product product = getFromCache(cacheKey);
        if (product != null) {
            return product;
        }

        String lockKey = "lock:" + cacheKey;
        if (lockService.tryLock(lockKey, 10)) {
            try {
                // 双重检查
                product = getFromCache(cacheKey);
                if (product != null) {
                    return product;
                }
                // 从数据库加载
                product = productRepository.findById(id).orElse(null);
                if (product != null) {
                    saveToCache(cacheKey, product, 3600);
                }
            } finally {
                lockService.unlock(lockKey);
            }
        }

        return product;
    }

    // 缓存穿透:布隆过滤器 或 空值缓存
    public User getUser(Long id) {
        String cacheKey = "user:" + id;

        // 空值缓存,防止穿透
        String nullValue = (String) redisTemplate.opsForValue().get(cacheKey + ":null");
        if ("1".equals(nullValue)) {
            return null;
        }

        User user = getFromCache(cacheKey);
        if (user == null) {
            user = userRepository.findById(id).orElse(null);
            if (user == null) {
                // 缓存空值,短时间有效
                redisTemplate.opsForValue().set(
                    cacheKey + ":null", "1", Duration.ofMinutes(5));
            } else {
                saveToCache(cacheKey, user, 3600);
            }
        }
        return user;
    }
}

常见坑点

⚠️ 坑 1:缓存和数据库不一致

// ❌ 先删缓存,后更新数据库(高并发下可能读到旧数据)
@CacheEvict(key = "#id")
public void update(User user) {
    userRepository.save(user);  // 如果此时其他请求查库,会写入旧缓存
}

// ✅ 方案1:延迟双删
public void update(User user) {
    redisTemplate.delete("user:" + user.getId());
    userRepository.save(user);
    try { Thread.sleep(100); } catch (Exception e) {}
    redisTemplate.delete("user:" + user.getId());
}

// ✅ 方案2:分布式事务(最终一致)
// ✅ 方案3:更新时不清缓存,只删除

⚠️ 坑 2:@Cacheable 不生效

// ❌ 类内部调用不走代理
@Service
public class UserService {

    public User getById(Long id) {
        return getFromDb(id);  // 不会走缓存
    }

    @Cacheable(key = "#id")
    public User getFromDb(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

// ✅ 注入自身代理
@Autowired
private UserService self;

public User getById(Long id) {
    return self.getFromDb(id);  // 走代理
}

⚠️ 坑 3:序列化问题

// ❌ 直接存储未实现序列化
@Cacheable(key = "#user.id")
public void cacheUser(User user) {  // User 必须实现 Serializable
}

// ✅ 确保实体可序列化
@Entity
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ...
}

面试题

Q1:缓存穿透、击穿、雪崩的区别?

参考答案

问题原因解决方案
穿透查询不存在的数据布隆过滤器 / 空值缓存
击穿热点 key 过期瞬间大量请求分布式锁 / 永不过期
雪崩大量 key 同时过期过期时间随机 / 多级缓存

Q2:Redis 和本地缓存如何选择?

参考答案

维度Redis本地缓存(Caffeine/Guava)
共享多实例共享各实例独立
容量受内存限制受 JVM 内存限制
性能略慢(网络)更快(本地)
适用分布式 / 大数据量热点数据 / 低延迟

建议:二级缓存组合使用

请求 → 本地缓存 → Redis → 数据库

Q3:如何保证缓存与数据库一致性?

参考答案

  1. Cache-aside(旁路缓存)

    • 读:缓存存在返回,不存在查库并写入缓存
    • 写:先更新数据库,再删除缓存
  2. 延迟双删

    • 更新时先删缓存
    • 更新数据库
    • 延迟一段时间后再删缓存
  3. 订阅 Binlog

    • 监听数据库变更
    • 异步更新缓存
  4. 分布式事务(强一致,但不推荐,性能差)