架构视角:缓存在系统架构中的定位

在现代分布式系统中,缓存已成为提升性能、降低数据库压力的核心组件。从架构师视角看,缓存不仅是技术选型问题,更是系统容量规划、数据一致性策略、故障容错设计的综合体现。一个优秀的缓存架构需要在性能、一致性、可用性之间找到最佳平衡点。

分布式缓存架构核心目标

  • 性能提升:将数据访问延迟从毫秒级降至微秒级
  • 容量扩展:支持水平扩展,应对数据量和并发增长
  • 高可用性:缓存故障不导致系统雪崩
  • 数据一致性:在性能与一致性之间做出合理权衡

缓存架构模式:从本地到分布式

多级缓存架构

生产环境通常采用多级缓存策略,形成从 CPU 到分布式缓存的完整层次:

// 多级缓存架构实现
@Component
public class MultiLevelCache {
    
    /**
     * L1: 本地 Caffeine 缓存(进程内)
     * - 访问延迟:~100ns
     * - 容量:受限于单节点内存
     * - 一致性:需处理缓存失效
     */
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(Duration.ofMinutes(5))
        .recordStats()
        .build();
    
    /**
     * L2: Redis 分布式缓存(进程间)
     * - 访问延迟:~1ms
     * - 容量:可水平扩展
     * - 一致性:集群内一致
     */
    private final StringRedisTemplate redisTemplate;
    
    /**
     * L3: 数据库(持久化存储)
     * - 访问延迟:~10ms
     * - 容量:持久化存储
     * - 一致性:强一致
     */
    private final DataRepository dataRepository;
    
    /**
     * 多级缓存读取策略
     */
    public <T> T get(String key, Class<T> type) {
        // L1: 本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) {
            return (T) value;
        }
        
        // L2: Redis 缓存
        String redisValue = redisTemplate.opsForValue().get(key);
        if (redisValue != null) {
            T result = JSON.parseObject(redisValue, type);
            // 回填本地缓存
            localCache.put(key, result);
            return result;
        }
        
        // L3: 数据库(带分布式锁防止缓存击穿)
        return loadFromDatabase(key, type);
    }
    
    /**
     * 防止缓存击穿的加载逻辑
     */
    private <T> T loadFromDatabase(String key, Class<T> type) {
        String lockKey = "lock:" + key;
        
        // 尝试获取分布式锁
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
        
        if (Boolean.TRUE.equals(locked)) {
            try {
                // 双重检查
                String cached = redisTemplate.opsForValue().get(key);
                if (cached != null) {
                    return JSON.parseObject(cached, type);
                }
                
                // 从数据库加载
                T value = dataRepository.findById(key, type);
                if (value != null) {
                    String json = JSON.toJSONString(value);
                    redisTemplate.opsForValue().set(key, json, Duration.ofHours(1));
                    localCache.put(key, value);
                }
                return value;
            } finally {
                redisTemplate.delete(lockKey);
            }
        } else {
            // 获取锁失败,短暂等待后重试
            Thread.sleep(100);
            return get(key, type);
        }
    }
}

缓存架构模式对比

模式 架构 优点 缺点 适用场景
本地缓存 Caffeine/Guava 极低延迟、无网络开销 多节点不一致、容量受限 配置数据、热点数据
分布式缓存 Redis/Memcached 数据共享、容量可扩展 网络延迟、单点瓶颈 会话、热点数据
多级缓存 L1+L2+L3 兼顾性能与一致性 架构复杂、失效困难 高并发读场景
客户端分片 一致性哈希 无中心节点、线性扩展 扩容时数据迁移 Memcached 集群
服务端分片 Redis Cluster 自动故障转移、数据均衡 架构复杂、跨节点事务受限 大规模 Redis 部署

Redis 高性能架构设计

Redis 数据结构与使用策略

Redis 提供了丰富的数据结构,合理选择数据结构是性能优化的基础:

// Redis 高性能数据结构应用
@Component
public class RedisHighPerformancePatterns {
    
    private final StringRedisTemplate redisTemplate;
    
    /**
     * 场景1:String - 缓存对象(配合压缩)
     */
    public void cacheObject(String key, Object value) {
        // 使用压缩减少内存占用
        byte[] compressed = compress(JSON.toJSONBytes(value));
        redisTemplate.opsForValue().set(key, Base64.encode(compressed));
    }
    
    /**
     * 场景2:Hash - 存储对象字段(节省内存)
     * 比多个String节省40%以上内存
     */
    public void cacheUserProfile(User user) {
        String key = "user:" + user.getId();
        Map<String, String> fields = new HashMap<>();
        fields.put("name", user.getName());
        fields.put("email", user.getEmail());
        fields.put("avatar", user.getAvatar());
        
        redisTemplate.opsForHash().putAll(key, fields);
        redisTemplate.expire(key, Duration.ofHours(1));
    }
    
    /**
     * 场景3:Bitmap - 签到/在线状态(极省内存)
     * 1亿用户1天的签到数据仅需约12MB
     */
    public void userCheckIn(String userId, LocalDate date) {
        String key = "checkin:" + date.format(DateTimeFormatter.BASIC_ISO_DATE);
        long offset = Long.parseLong(userId);
        redisTemplate.opsForValue().setBit(key, offset, true);
    }
    
    public long getCheckInCount(LocalDate date) {
        String key = "checkin:" + date.format(DateTimeFormatter.BASIC_ISO_DATE);
        return redisTemplate.execute(
            (RedisCallback<Long>) conn -> conn.stringCommands().bitCount(key.getBytes())
        );
    }
    
    /**
     * 场景4:HyperLogLog - UV统计(允许0.81%误差)
     * 1亿UV仅需12KB内存
     */
    public void recordPageView(String pageId, String userId) {
        String key = "pv:" + pageId;
        redisTemplate.opsForHyperLogLog().add(key, userId);
    }
    
    public long getUniqueVisitors(String pageId) {
        return redisTemplate.opsForHyperLogLog().size("pv:" + pageId);
    }
    
    /**
     * 场景5:Sorted Set - 排行榜/延迟队列
     */
    public void updateLeaderboard(String gameId, String playerId, double score) {
        String key = "leaderboard:" + gameId;
        redisTemplate.opsForZSet().add(key, playerId, score);
    }
    
    public Set<ZSetOperations.TypedTuple<String>> getTopPlayers(String gameId, int n) {
        String key = "leaderboard:" + gameId;
        return redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, n - 1);
    }
    
    /**
     * 场景6:Pipeline 批量操作(减少RTT)
     */
    public List<Object> batchGet(List<String> keys) {
        return redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
            for (String key : keys) {
                connection.stringCommands().get(key.getBytes());
            }
            return null;
        });
    }
}

Redis 集群架构设计

// Redis Cluster 架构配置
@Configuration
public class RedisClusterConfig {
    
    /**
     * Redis Cluster 架构要点:
     * 1. 数据分片:16384个slot分配到多个节点
     * 2. 主从复制:每个主节点有1-N个从节点
     * 3. 故障转移:哨兵或Cluster自动故障转移
     * 4. 客户端路由:MOVED/ASK重定向处理
     */
    
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        // 集群节点配置
        RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(
            Arrays.asList(
                "redis-node1:6379",
                "redis-node2:6379",
                "redis-node3:6379",
                "redis-node4:6379",
                "redis-node5:6379",
                "redis-node6:6379"
            )
        );
        
        // 客户端配置
        ClientOptions clientOptions = ClientOptions.builder()
            .socketOptions(SocketOptions.builder()
                .connectTimeout(Duration.ofMillis(100))
                .keepAlive(true)
                .build())
            .timeoutOptions(TimeoutOptions.builder()
                .timeoutCommands(true)
                .build())
            .build();
        
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .clientOptions(clientOptions)
            .readFrom(ReadFrom.REPLICA_PREFERRED) // 优先从从节点读
            .build();
        
        return new LettuceConnectionFactory(clusterConfig, clientConfig);
    }
}

缓存问题与解决方案

缓存穿透、击穿、雪崩

缓存架构需要重点防范三类经典问题:

// 缓存问题防护实现
@Component
public class CacheProtection {
    
    private final StringRedisTemplate redisTemplate;
    
    /**
     * 1. 缓存穿透防护:查询不存在的数据
     * 解决方案:布隆过滤器 + 空值缓存
     */
    @Component
    public class CachePenetrationProtection {
        
        private final RBloomFilter<String> bloomFilter;
        
        public <T> T getWithBloomFilter(String key, Class<T> type) {
            // 布隆过滤器检查
            if (!bloomFilter.contains(key)) {
                return null; // 数据肯定不存在
            }
            
            // 正常查询缓存
            String value = redisTemplate.opsForValue().get(key);
            if (value != null) {
                return JSON.parseObject(value, type);
            }
            
            return null;
        }
        
        /**
         * 空值缓存策略
         */
        public void cacheNullValue(String key) {
            // 缓存空值,设置较短过期时间
            redisTemplate.opsForValue().set(key, "null", Duration.ofMinutes(5));
        }
    }
    
    /**
     * 2. 缓存击穿防护:热点key过期瞬间的高并发
     * 解决方案:互斥锁 + 逻辑过期
     */
    public <T> T getWithMutex(String key, Class<T> type, Supplier<T> dbLoader) {
        String value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            return JSON.parseObject(value, type);
        }
        
        // 获取互斥锁
        String lockKey = "lock:" + key;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
        
        if (Boolean.TRUE.equals(locked)) {
            try {
                // 双重检查
                value = redisTemplate.opsForValue().get(key);
                if (value != null) {
                    return JSON.parseObject(value, type);
                }
                
                // 从数据库加载
                T data = dbLoader.get();
                if (data != null) {
                    redisTemplate.opsForValue().set(key, JSON.toJSONString(data), 
                        Duration.ofMinutes(30));
                }
                return data;
            } finally {
                redisTemplate.delete(lockKey);
            }
        } else {
            // 获取锁失败,短暂等待后重试
            Thread.sleep(50);
            return getWithMutex(key, type, dbLoader);
        }
    }
    
    /**
     * 3. 缓存雪崩防护:大量key同时过期
     * 解决方案:过期时间随机化 + 多级缓存 + 熔断降级
     */
    public void setWithRandomExpire(String key, Object value, int baseExpireMinutes) {
        // 基础过期时间 + 随机偏移(0-10分钟)
        int randomOffset = ThreadLocalRandom.current().nextInt(10);
        Duration expireTime = Duration.ofMinutes(baseExpireMinutes + randomOffset);
        
        redisTemplate.opsForValue().set(key, JSON.toJSONString(value), expireTime);
    }
}

缓存问题防护策略总结

  • 缓存穿透:布隆过滤器预过滤 + 空值缓存短时效
  • 缓存击穿:分布式互斥锁 + 热点数据永不过期
  • 缓存雪崩:过期时间随机化 + 多级缓存 + 熔断降级
  • 大Key问题:数据分片 + 压缩存储 + 定期清理
  • 热Key问题:本地缓存 + 读写分离 + Key拆分

缓存一致性策略

缓存更新模式

// 缓存一致性策略实现
@Component
public class CacheConsistency {
    
    /**
     * 模式1:Cache-Aside(旁路缓存)- 最常用
     * 读:先读缓存,未命中则读DB并写入缓存
     * 写:先写DB,再删缓存
     */
    public class CacheAsidePattern {
        
        public <T> T read(String key, Class<T> type, Supplier<T> dbLoader) {
            T cached = getFromCache(key, type);
            if (cached != null) {
                return cached;
            }
            
            T data = dbLoader.get();
            if (data != null) {
                putToCache(key, data);
            }
            return data;
        }
        
        public void write(String key, Object data, Runnable dbUpdater) {
            // 1. 先更新数据库
            dbUpdater.run();
            
            // 2. 再删除缓存(非更新,避免并发写覆盖)
            deleteCache(key);
            
            // 3. 发送缓存失效消息(保证最终一致性)
            publishCacheInvalidate(key);
        }
    }
    
    /**
     * 模式2:Read-Through(读穿透)
     * 缓存层自动从DB加载
     */
    public class ReadThroughPattern {
        
        public <T> T read(String key, Class<T> type) {
            // 使用Caffeine的LoadingCache自动加载
            LoadingCache<String, T> cache = Caffeine.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(Duration.ofMinutes(10))
                .build(k -> loadFromDatabase(k, type));
            
            return cache.get(key);
        }
    }
    
    /**
     * 模式3:Write-Through(写穿透)
     * 同步更新缓存和DB
     */
    public void writeThrough(String key, Object value) {
        // 同步双写,保证强一致性但性能较低
        redisTemplate.opsForValue().set(key, JSON.toJSONString(value));
        dataRepository.save(key, value);
    }
    
    /**
     * 模式4:Write-Behind(异步写)
     * 先写缓存,异步批量写DB
     */
    public class WriteBehindPattern {
        
        private final BlockingQueue<WriteOp> writeQueue = 
            new LinkedBlockingQueue<>(10000);
        
        public void write(String key, Object value) {
            // 1. 立即写缓存
            redisTemplate.opsForValue().set(key, JSON.toJSONString(value));
            
            // 2. 异步写DB
            writeQueue.offer(new WriteOp(key, value));
        }
        
        @Scheduled(fixedRate = 5000)
        public void batchWriteToDatabase() {
            List<WriteOp> batch = new ArrayList<>();
            writeQueue.drainTo(batch, 100);
            
            if (!batch.isEmpty()) {
                dataRepository.batchSave(batch);
            }
        }
    }
}

分布式缓存一致性方案

// 基于 Canal 的缓存一致性方案
@Component
public class CanalCacheSync {
    
    /**
     * Canal 监听 MySQL binlog,异步更新缓存
     * 实现最终一致性,无需业务代码侵入
     */
    @CanalListener(destination = "example")
    public void onDatabaseChange(CanalEntry.Entry entry) {
        CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
        
        for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
            String tableName = entry.getHeader().getTableName();
            
            if (rowChange.getEventType() == CanalEntry.EventType.UPDATE ||
                rowChange.getEventType() == CanalEntry.EventType.DELETE) {
                
                // 提取主键
                String primaryKey = extractPrimaryKey(rowData.getBeforeColumnsList());
                String cacheKey = tableName + ":" + primaryKey;
                
                // 删除缓存
                redisTemplate.delete(cacheKey);
                
                // 发送本地缓存失效消息
                cacheInvalidatePublisher.publish(new CacheInvalidateEvent(cacheKey));
            }
        }
    }
}

架构决策总结

决策点 推荐方案 适用场景
缓存架构 多级缓存(Caffeine + Redis) 高并发读场景
Redis部署 Redis Cluster 6节点+ 大规模生产环境
缓存更新 Cache-Aside + 延迟双删 大多数业务场景
一致性保障 Canal + 消息队列 强一致性要求
穿透防护 布隆过滤器 + 空值缓存 高并发查询
击穿防护 分布式锁 + 热点预加载 热点数据场景
雪崩防护 随机过期 + 熔断降级 大规模缓存集群

缓存架构反模式警示

  • 缓存一切:不区分冷热数据,浪费资源
  • 忽视一致性:缓存与DB长期不一致
  • 单点部署:无高可用设计,故障即雪崩
  • 大Key热Key:未监控和治理,性能隐患
  • 无降级策略:缓存故障时系统不可用

总结

分布式缓存架构设计是一项系统工程,需要从数据特征、访问模式、一致性要求、故障容忍等多个维度综合考虑。从多级缓存的层次设计到 Redis 的数据结构优化,从穿透击穿的防护到最终一致性的保障,每个环节都需要精心设计和持续优化。

优秀的缓存架构应该像空气一样存在:在正常情况下提供极致性能,在异常情况下优雅降级,始终保障系统的稳定运行。架构师的核心职责是在复杂约束条件下,找到最适合业务场景的缓存策略组合。