架构视角:缓存在系统架构中的定位
在现代分布式系统中,缓存已成为提升性能、降低数据库压力的核心组件。从架构师视角看,缓存不仅是技术选型问题,更是系统容量规划、数据一致性策略、故障容错设计的综合体现。一个优秀的缓存架构需要在性能、一致性、可用性之间找到最佳平衡点。
分布式缓存架构核心目标
- 性能提升:将数据访问延迟从毫秒级降至微秒级
- 容量扩展:支持水平扩展,应对数据量和并发增长
- 高可用性:缓存故障不导致系统雪崩
- 数据一致性:在性能与一致性之间做出合理权衡
缓存架构模式:从本地到分布式
多级缓存架构
生产环境通常采用多级缓存策略,形成从 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 的数据结构优化,从穿透击穿的防护到最终一致性的保障,每个环节都需要精心设计和持续优化。
优秀的缓存架构应该像空气一样存在:在正常情况下提供极致性能,在异常情况下优雅降级,始终保障系统的稳定运行。架构师的核心职责是在复杂约束条件下,找到最适合业务场景的缓存策略组合。