Skip to content
清晨的一缕阳光
返回

缓存策略设计实战

缓存策略设计实战

缓存是提升系统性能最有效的手段。合理的缓存设计可以将响应时间从几百毫秒降低到几毫秒。但缓存也带来了一致性、穿透、雪崩等问题。本文详解缓存架构设计的核心技术和最佳实践。

一、缓存架构设计

1.1 多级缓存架构

graph TB
    User[用户请求] --> L1[L1: CDN 缓存<br/>静态资源 TTL:1 天 -1 年<br/>命中率 80%+]
    L1 --> L2[L2: Nginx 缓存<br/>API 响应 TTL:1 分钟 -1 小时<br/>命中率 60%+]
    L2 --> L3[L3: 本地缓存 Caffeine<br/>热点数据 TTL:1 秒 -10 分钟<br/>命中率 90%+]
    L3 --> L4[L4: Redis 集群<br/>业务数据 TTL:5 分钟 -24 小时<br/>命中率 70%+]
    L4 --> L5[L5: 数据库 MySQL<br/>持久化数据/复杂查询/事务操作]

1.2 缓存选型

缓存类型代表产品特点适用场景
本地缓存Caffeine、Guava最快、无网络开销、容量有限热点数据、配置数据
分布式缓存Redis、Memcached共享、容量大、网络开销业务数据、Session
数据库缓存Query Cache自动、粗粒度SQL 查询结果
CDN 缓存阿里云 CDN边缘节点、静态资源图片、视频、静态文件

二、本地缓存实战

2.1 Caffeine 使用

/**
 * Caffeine 本地缓存配置
 */
@Configuration
public class CaffeineConfig {
    
    /**
     * 用户信息缓存
     */
    @Bean
    public Cache<String, User> userCache() {
        return Caffeine.newBuilder()
            .maximumSize(10000)              // 最大 1 万条
            .expireAfterWrite(10, TimeUnit.MINUTES)  // 写入后 10 分钟过期
            .expireAfterAccess(5, TimeUnit.MINUTES)  // 访问后 5 分钟过期
            .recordStats()                   // 记录统计信息
            .build();
    }
    
    /**
     * 配置数据缓存(永不过期,手动刷新)
     */
    @Bean
    public Cache<String, Object> configCache() {
        return Caffeine.newBuilder()
            .maximumSize(1000)
            .build();
    }
    
    /**
     * 热点数据缓存(基于访问频率)
     */
    @Bean
    public Cache<String, Object> hotCache() {
        return Caffeine.newBuilder()
            .maximumSize(5000)
            .expireAfterAccess(1, TimeUnit.MINUTES)
            .weigher((key, value) -> {
                // 大对象占用更多权重
                if (value instanceof String) {
                    return ((String) value).length() / 100;
                }
                return 1;
            })
            .build();
    }
}

/**
 * 使用示例
 */
@Service
public class UserService {
    
    @Autowired
    private Cache<String, User> userCache;
    
    @Autowired
    private UserMapper userMapper;
    
    public User getUserById(String userId) {
        // 从缓存获取
        User user = userCache.getIfPresent(userId);
        
        if (user == null) {
            // 缓存未命中,查询数据库
            user = userMapper.selectById(userId);
            
            if (user != null) {
                // 写入缓存
                userCache.put(userId, user);
            }
        }
        
        return user;
    }
    
    public void updateUser(User user) {
        // 更新数据库
        userMapper.update(user);
        
        // 删除缓存(保证一致性)
        userCache.invalidate(user.getId());
    }
    
    /**
     * 批量查询(避免缓存击穿)
     */
    public List<User> getUsersByIds(List<String> userIds) {
        // 批量从缓存获取
        Map<String, User> cachedUsers = userCache.getAllPresent(userIds);
        
        // 找出未命中的 ID
        List<String> missedIds = userIds.stream()
            .filter(id -> !cachedUsers.containsKey(id))
            .collect(Collectors.toList());
        
        // 批量查询数据库
        if (!missedIds.isEmpty()) {
            List<User> dbUsers = userMapper.selectByIds(missedIds);
            
            // 批量写入缓存
            userCache.putAll(dbUsers.stream()
                .collect(Collectors.toMap(User::getId, u -> u)));
            
            // 合并结果
            cachedUsers.putAll(dbUsers.stream()
                .collect(Collectors.toMap(User::getId, u -> u)));
        }
        
        return userIds.stream()
            .map(cachedUsers::get)
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }
}

2.2 缓存监听

/**
 * 缓存监听器
 */
@Component
public class CacheListener {
    
    @PostConstruct
    public void init() {
        // 监听缓存移除
        userCache.asMap().forEach((key, value) -> {
            log.info("缓存 key: {}, value: {}", key, value);
        });
    }
    
    @Autowired
    private Cache<String, User> userCache;
    
    /**
     * 缓存移除回调
     */
    public void onRemoval(String key, User user, RemovalCause cause) {
        switch (cause) {
            case EXPIRED:
                log.info("缓存过期:key={}, user={}", key, user.getName());
                break;
            case SIZE:
                log.info("缓存淘汰:key={}, user={}", key, user.getName());
                break;
            case EXPLICIT:
                log.info("缓存删除:key={}, user={}", key, user.getName());
                break;
            default:
                break;
        }
    }
}

三、Redis 缓存实战

3.1 缓存设计模式

/**
 * 缓存设计模式
 */
@Service
public class CachePatternService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 模式 1:Cache Aside(旁路缓存)
     * 
     * 读:先读缓存,未命中则读 DB 并写入缓存
     * 写:先写 DB,再删除缓存
     * 
     * 适用:大多数场景
     */
    public User getUserById(Long id) {
        String key = "user:" + id;
        
        // 读缓存
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }
        
        // 读数据库
        user = userMapper.selectById(id);
        if (user != null) {
            // 写入缓存(设置过期时间)
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        }
        
        return user;
    }
    
    public void updateUser(User user) {
        // 先写数据库
        userMapper.update(user);
        
        // 再删除缓存(下次读取时更新)
        String key = "user:" + user.getId();
        redisTemplate.delete(key);
    }
    
    /**
     * 模式 2:Read Through(读穿透)
     * 
     * 缓存代理数据库,应用只操作缓存
     * 缓存未命中时,缓存系统自动加载数据
     * 
     * 适用:缓存系统支持 Read Through(如 Redis 未原生支持,需自行实现)
     */
    public User getUserWithReadThrough(Long id) {
        String key = "user:" + id;
        
        return (User) redisTemplate.execute((RedisCallback<User>) connection -> {
            // 尝试从缓存获取
            byte[] value = connection.get(key.getBytes());
            if (value != null) {
                return deserialize(value);
            }
            
            // 缓存未命中,加载数据
            User user = userMapper.selectById(id);
            if (user != null) {
                connection.setEx(key.getBytes(), 1800, serialize(user));
            }
            
            return user;
        });
    }
    
    /**
     * 模式 3:Write Behind(异步回写)
     * 
     * 先更新缓存,再异步写入数据库
     * 
     * 适用:高并发写场景,允许短暂不一致
     */
    @Autowired
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
    
    private final Queue<User> writeQueue = new ConcurrentLinkedQueue<>();
    
    public void updateUserWithWriteBehind(User user) {
        // 先更新缓存
        String key = "user:" + user.getId();
        redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        
        // 加入异步队列
        writeQueue.offer(user);
    }
    
    @PostConstruct
    public void initWriteBehind() {
        // 每秒批量写入数据库
        scheduler.scheduleAtFixedRate(this::batchWriteToDb, 1, 1, TimeUnit.SECONDS);
    }
    
    private void batchWriteToDb() {
        List<User> batch = new ArrayList<>();
        User user;
        while ((user = writeQueue.poll()) != null && batch.size() < 100) {
            batch.add(user);
        }
        
        if (!batch.isEmpty()) {
            userMapper.batchUpdate(batch);
        }
    }
    
    private byte[] serialize(Object obj) {
        // 序列化实现
        return SerializationUtils.serialize((Serializable) obj);
    }
    
    private Object deserialize(byte[] bytes) {
        // 反序列化实现
        return SerializationUtils.deserialize(bytes);
    }
}

3.2 缓存一致性保障

/**
 * 缓存一致性保障方案
 */
@Service
public class CacheConsistencyService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 方案 1:延时双删
     * 
     * 1. 先删除缓存
     * 2. 更新数据库
     * 3. 休眠 N 毫秒
     * 4. 再次删除缓存
     * 
     * 目的:防止更新 DB 时,有读请求写入旧数据
     */
    public void updateUserWithDoubleDelete(User user) {
        String key = "user:" + user.getId();
        
        // 1. 删除缓存
        redisTemplate.delete(key);
        
        // 2. 更新数据库
        userMapper.update(user);
        
        // 3. 延时双删(异步执行)
        CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(500);  // 休眠 500ms
                redisTemplate.delete(key);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
    
    /**
     * 方案 2:监听 Binlog 删除缓存
     * 
     * 使用 Canal 监听 MySQL Binlog
     * 数据变更后自动删除缓存
     */
    @StreamListener("canal:user")
    public void handleUserChange(UserChangeMessage message) {
        if (message.getEventType() == "UPDATE" || message.getEventType() == "DELETE") {
            String key = "user:" + message.getUserId();
            redisTemplate.delete(key);
        }
    }
    
    /**
     * 方案 3:消息队列保证最终一致性
     * 
     * 更新 DB 后发送消息
     * 消费者删除缓存
     * 失败重试
     */
    public void updateUserWithMQ(User user) {
        // 更新数据库
        userMapper.update(user);
        
        // 发送删除缓存消息
        CacheMessage message = new CacheMessage();
        message.setKey("user:" + user.getId());
        message.setOperation("DELETE");
        
        rocketMQTemplate.send("cache_topic", message);
    }
    
    @RocketMQMessageListener(
        topic = "cache_topic",
        consumerGroup = "cache_consumer_group"
    )
    public class CacheConsumer implements RocketMQListener<CacheMessage> {
        
        @Override
        public void onMessage(CacheMessage message) {
            if ("DELETE".equals(message.getOperation())) {
                redisTemplate.delete(message.getKey());
            } else if ("UPDATE".equals(message.getOperation())) {
                // 重新加载缓存
                reloadCache(message.getKey());
            }
        }
        
        private void reloadCache(String key) {
            // 从数据库加载数据并写入缓存
            Long userId = Long.valueOf(key.split(":")[1]);
            User user = userMapper.selectById(userId);
            if (user != null) {
                redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
            }
        }
    }
    
    /**
     * 方案 4:分布式锁保证一致性
     * 
     * 更新时加锁,防止并发读写
     */
    public void updateUserWithLock(User user) {
        String lockKey = "lock:user:" + user.getId();
        String cacheKey = "user:" + user.getId();
        
        // 获取分布式锁
        RLock lock = redissonClient.getLock(lockKey);
        if (lock.tryLock()) {
            try {
                // 更新数据库
                userMapper.update(user);
                
                // 删除缓存
                redisTemplate.delete(cacheKey);
            } finally {
                lock.unlock();
            }
        } else {
            throw new BusinessException("系统繁忙,请稍后重试");
        }
    }
    
    @Autowired
    private RedissonClient redissonClient;
}

3.3 缓存预热

/**
 * 缓存预热服务
 */
@Service
public class CachePreloadService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private ProductMapper productMapper;
    
    /**
     * 全量预热(系统启动时)
     */
    @PostConstruct
    public void preloadAll() {
        // 异步执行,避免阻塞启动
        CompletableFuture.runAsync(() -> {
            log.info("开始缓存预热...");
            
            // 预热用户数据(VIP 用户)
            preloadVipUsers();
            
            // 预热商品数据(热销商品)
            preloadHotProducts();
            
            // 预热配置数据
            preloadConfigs();
            
            log.info("缓存预热完成");
        });
    }
    
    /**
     * 定时预热(每天凌晨)
     */
    @Scheduled(cron = "0 0 3 * * ?")
    public void scheduledPreload() {
        log.info("定时缓存预热开始");
        preloadHotProducts();
    }
    
    private void preloadVipUsers() {
        List<User> vipUsers = userMapper.selectVipUsers();
        for (User user : vipUsers) {
            String key = "user:" + user.getId();
            redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
        }
        log.info("VIP 用户预热完成,共{}条", vipUsers.size());
    }
    
    private void preloadHotProducts() {
        List<Product> hotProducts = productMapper.selectHotProducts(100);
        for (Product product : hotProducts) {
            String key = "product:" + product.getId();
            redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
        }
        log.info("热销商品预热完成,共{}条", hotProducts.size());
    }
    
    private void preloadConfigs() {
        List<Config> configs = configMapper.selectAll();
        for (Config config : configs) {
            String key = "config:" + config.getKey();
            redisTemplate.opsForValue().set(key, config.getValue());
        }
        log.info("配置数据预热完成,共{}条", configs.size());
    }
}

四、热点数据处理

4.1 热点发现

/**
 * 热点数据发现
 */
@Component
public class HotKeyDetector {
    
    private final ConcurrentHashMap<String, LongAdder> keyCounter = new ConcurrentHashMap<>();
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    @PostConstruct
    public void init() {
        // 每分钟统计一次热点 key
        scheduler.scheduleAtFixedRate(this::detectHotKeys, 1, 1, TimeUnit.MINUTES);
    }
    
    /**
     * 记录 key 访问
     */
    public void recordAccess(String key) {
        LongAdder counter = keyCounter.computeIfAbsent(key, k -> new LongAdder());
        counter.increment();
    }
    
    /**
     * 发现热点 key
     */
    private void detectHotKeys() {
        // 找出访问频率超过阈值的 key
        List<String> hotKeys = keyCounter.entrySet().stream()
            .filter(entry -> entry.getValue().sum() > 1000)  // 阈值:1000 次/分钟
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
        
        if (!hotKeys.isEmpty()) {
            log.info("发现热点 Key: {}", hotKeys);
            
            // 推送到本地缓存
            for (String key : hotKeys) {
                preloadToCaffeine(key);
            }
            
            // 发送到消息队列(其他节点同步)
            rocketMQTemplate.send("hotkey_topic", hotKeys);
        }
        
        // 重置计数器
        keyCounter.clear();
    }
    
    private void preloadToCaffeine(String key) {
        // 从 Redis 加载数据到本地缓存
        Object value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            localCache.put(key, value);
        }
    }
    
    @StreamListener("hotkey_topic")
    public void handleHotKey(List<String> hotKeys) {
        for (String key : hotKeys) {
            preloadToCaffeine(key);
        }
    }
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private Cache<String, Object> localCache;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
}

4.2 热点缓存

/**
 * 热点数据缓存服务
 */
@Service
public class HotKeyCacheService {
    
    @Autowired
    private Cache<String, Object> localCache;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 获取数据(多级缓存)
     */
    public Object get(String key) {
        // L1: 本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // L2: Redis 缓存
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            // 回写到本地缓存
            localCache.put(key, value);
            return value;
        }
        
        return null;
    }
    
    /**
     * 热点数据特殊处理
     */
    public Object getHotKey(String key) {
        // 热点数据永不过期(本地缓存)
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // Redis 缓存(较长 TTL)
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            // 永久缓存到本地
            localCache.put(key, value);
            return value;
        }
        
        return null;
    }
}

五、总结

5.1 核心要点

  1. 多级缓存:CDN → Nginx → 本地 → Redis → DB
  2. 更新策略:Cache Aside 最常用、Read Through 简化代码
  3. 一致性保障:延时双删、Binlog 监听、MQ 保证
  4. 热点处理:发现机制、本地缓存、永不过期
  5. 缓存预热:启动预热、定时预热、手动触发

5.2 最佳实践

mindmap
  root((缓存设计最佳实践))
    推荐做法
      设置合理的过期时间
      使用 Cache Aside 模式
      热点数据使用本地缓存
      批量查询避免缓存击穿
      缓存预热提升体验
      监控缓存命中率
    避免做法
      缓存永不过期
      大 key>10KB
      缓存穿透
      缓存雪崩
      缓存与 DB 长期不一致

缓存是双刃剑,用得好提升性能,用不好带来灾难。合理设计、持续监控、及时优化是关键。


分享这篇文章到:

上一篇文章
领域驱动设计实战
下一篇文章
高并发系统设计实战