Skip to main content

分布式缓存架构详解

分布式缓存是现代互联网应用的核心组件,用于提升读性能、削峰填谷与降低后端压力。本章将详细介绍分布式缓存的架构设计、一致性策略、性能优化和最佳实践。

缓存的价值
  • 性能提升:将热点数据存储在内存中,大幅提升访问速度
  • 压力缓解:减少对数据库的直接访问,降低数据库压力
  • 成本优化:通过缓存减少昂贵的数据库查询和计算
  • 用户体验:提升系统响应速度,改善用户体验

1. 缓存架构设计

1.1 缓存分类

分布式缓存按照部署位置和访问方式可以分为以下几类:

本地缓存(Local Cache)

  • 特点:进程内缓存,访问速度极快,容量有限
  • 实现:Guava Caches、Caffeine、Ehcache
  • 适用场景:高频访问的配置信息、计算结果
本地缓存示例
java
1public class LocalCacheExample {
2 // 使用Caffeine构建高性能本地缓存
3 private final Cache<String, User> userCache = Caffeine.newBuilder()
4 .maximumSize(10_000) // 最大容量
5 .expireAfterWrite(1, TimeUnit.HOURS) // 写入后1小时过期
6 .expireAfterAccess(30, TimeUnit.MINUTES) // 访问后30分钟过期
7 .recordStats() // 记录统计信息
8 .build();
9
10 public User getUser(String userId) {
11 return userCache.get(userId, key -> {
12 // 缓存未命中时的加载逻辑
13 return loadUserFromDatabase(key);
14 });
15 }
16
17 private User loadUserFromDatabase(String userId) {
18 // 从数据库加载用户信息
19 return userRepository.findById(userId);
20 }
21}

远程缓存(Remote Cache)

  • 特点:集中管理的分布式缓存,容量大,支持集群
  • 实现:Redis、Memcached、Hazelcast
  • 适用场景:跨服务共享数据、大规模数据缓存
Redis缓存示例
java
1@Service
2public class RedisCacheService {
3 @Autowired
4 private RedisTemplate<String, Object> redisTemplate;
5
6 public User getUser(String userId) {
7 String key = "user:" + userId;
8
9 // 先从缓存获取
10 User user = (User) redisTemplate.opsForValue().get(key);
11 if (user != null) {
12 return user;
13 }
14
15 // 缓存未命中,从数据库加载
16 user = userRepository.findById(userId);
17 if (user != null) {
18 // 设置缓存,过期时间30分钟
19 redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
20 }
21
22 return user;
23 }
24}

多级缓存(Multi-Level Cache)

  • 特点:本地缓存 + 远程缓存的组合,形成缓存层次
  • 优势:结合本地缓存的快速访问和远程缓存的大容量
  • 挑战:缓存一致性的保证
多级缓存示例
java
1public class MultiLevelCache {
2 private final Cache<String, Object> localCache; // 本地缓存
3 private final RedisTemplate<String, Object> redisCache; // 远程缓存
4
5 public Object get(String key) {
6 // 第一级:本地缓存
7 Object value = localCache.getIfPresent(key);
8 if (value != null) {
9 return value;
10 }
11
12 // 第二级:远程缓存
13 value = redisCache.opsForValue().get(key);
14 if (value != null) {
15 // 回填本地缓存
16 localCache.put(key, value);
17 return value;
18 }
19
20 // 第三级:数据库
21 value = loadFromDatabase(key);
22 if (value != null) {
23 // 同时更新两级缓存
24 redisCache.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
25 localCache.put(key, value);
26 }
27
28 return value;
29 }
30}

1.2 缓存读写模式

Cache-Aside模式(旁路缓存)

  • 原理:应用程序直接管理缓存,先查缓存,未命中则查数据库
  • 优点:实现简单,缓存策略灵活
  • 缺点:需要手动管理缓存一致性
Cache-Aside模式示例
java
1@Service
2public class CacheAsideService {
3
4 public User getUser(String userId) {
5 // 1. 先查缓存
6 String cacheKey = "user:" + userId;
7 User user = (User) redisTemplate.opsForValue().get(cacheKey);
8
9 if (user != null) {
10 return user;
11 }
12
13 // 2. 缓存未命中,查数据库
14 user = userRepository.findById(userId);
15
16 if (user != null) {
17 // 3. 更新缓存
18 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
19 }
20
21 return user;
22 }
23
24 public void updateUser(User user) {
25 // 1. 更新数据库
26 userRepository.save(user);
27
28 // 2. 删除缓存(Cache-Aside的写策略)
29 String cacheKey = "user:" + user.getId();
30 redisTemplate.delete(cacheKey);
31 }
32}

Read/Write-Through模式(读写穿透)

  • 原理:缓存作为数据库的代理,所有读写都经过缓存
  • 优点:缓存一致性容易保证
  • 缺点:增加了系统复杂性
Read/Write-Through模式示例
java
1@Service
2public class WriteThroughService {
3
4 public User getUser(String userId) {
5 // 直接通过缓存读取,缓存负责与数据库交互
6 return cacheManager.getUser(userId);
7 }
8
9 public void updateUser(User user) {
10 // 直接写入缓存,缓存负责同步到数据库
11 cacheManager.updateUser(user);
12 }
13}

Write-Behind模式(异步写入)

  • 原理:写操作先更新缓存,然后异步批量写入数据库
  • 优点:写入性能高,减少数据库压力
  • 缺点:数据可能丢失,一致性较弱
Write-Behind模式示例
java
1@Service
2public class WriteBehindService {
3 private final Queue<WriteOperation> writeQueue = new ConcurrentLinkedQueue<>();
4
5 public void updateUser(User user) {
6 // 1. 立即更新缓存
7 String cacheKey = "user:" + user.getId();
8 redisTemplate.opsForValue().set(cacheKey, user);
9
10 // 2. 异步写入数据库
11 writeQueue.offer(new WriteOperation(user));
12 }
13
14 @Scheduled(fixedRate = 5000) // 每5秒批量写入
15 public void batchWrite() {
16 List<WriteOperation> operations = new ArrayList<>();
17 WriteOperation op;
18
19 // 收集待写入的操作
20 while ((op = writeQueue.poll()) != null) {
21 operations.add(op);
22 }
23
24 // 批量写入数据库
25 if (!operations.isEmpty()) {
26 userRepository.saveAll(operations.stream()
27 .map(WriteOperation::getUser)
28 .collect(Collectors.toList()));
29 }
30 }
31}

2. 缓存策略与优化

2.1 缓存键设计

良好的缓存键设计是缓存系统成功的关键因素之一。

键命名规范

  • 层级结构:使用冒号分隔,形成层级关系
  • 业务标识:包含业务模块、实体类型、ID等信息
  • 版本控制:支持缓存版本升级和批量失效
缓存键设计示例
java
1public class CacheKeyGenerator {
2
3 // 用户信息缓存键
4 public static String userKey(String userId) {
5 return String.format("user:info:%s", userId);
6 }
7
8 // 用户权限缓存键
9 public static String userPermissionKey(String userId) {
10 return String.format("user:permission:%s", userId);
11 }
12
13 // 商品详情缓存键
14 public static String productKey(String productId) {
15 return String.format("product:detail:%s", productId);
16 }
17
18 // 商品列表缓存键(支持分页)
19 public static String productListKey(int page, int size, String category) {
20 return String.format("product:list:%s:%d:%d", category, page, size);
21 }
22
23 // 带版本的缓存键
24 public static String versionedKey(String baseKey, String version) {
25 return String.format("%s:v%s", baseKey, version);
26 }
27}

键长度优化

键长度优化示例
java
1public class CompactCacheKeyGenerator {
2
3 // 使用短前缀
4 private static final String USER_PREFIX = "u";
5 private static final String PRODUCT_PREFIX = "p";
6 private static final String ORDER_PREFIX = "o";
7
8 // 压缩的键格式
9 public static String compactUserKey(String userId) {
10 return String.format("%s:%s", USER_PREFIX, userId);
11 }
12
13 public static String compactProductKey(String productId) {
14 return String.format("%s:%s", PRODUCT_PREFIX, productId);
15 }
16
17 // 使用哈希值减少键长度
18 public static String hashedKey(String originalKey) {
19 return String.valueOf(originalKey.hashCode());
20 }
21}

2.2 过期策略

TTL(Time To Live)策略

  • 固定TTL:所有缓存项使用相同的过期时间
  • 动态TTL:根据数据特性设置不同的过期时间
  • 随机抖动:避免缓存雪崩,错峰过期
TTL策略示例
java
1public class TTLStrategy {
2
3 // 固定TTL
4 public static final Duration DEFAULT_TTL = Duration.ofMinutes(30);
5
6 // 动态TTL策略
7 public static Duration getTTL(String key, Object value) {
8 if (key.startsWith("user:")) {
9 return Duration.ofHours(1); // 用户信息1小时
10 } else if (key.startsWith("product:")) {
11 return Duration.ofMinutes(10); // 商品信息10分钟
12 } else if (key.startsWith("config:")) {
13 return Duration.ofDays(1); // 配置信息1天
14 }
15 return DEFAULT_TTL;
16 }
17
18 // 随机抖动TTL
19 public static Duration getRandomizedTTL(Duration baseTTL) {
20 long baseSeconds = baseTTL.getSeconds();
21 long randomOffset = (long) (baseSeconds * 0.1 * Math.random()); // 10%随机偏移
22 return Duration.ofSeconds(baseSeconds + randomOffset);
23 }
24
25 // 基于访问频率的TTL
26 public static Duration getFrequencyBasedTTL(int accessCount) {
27 if (accessCount > 1000) {
28 return Duration.ofHours(2); // 高频访问,延长TTL
29 } else if (accessCount > 100) {
30 return Duration.ofMinutes(30); // 中频访问,标准TTL
31 } else {
32 return Duration.ofMinutes(5); // 低频访问,短TTL
33 }
34 }
35}

2.3 热点保护策略

互斥锁保护

防止缓存击穿,确保同一时间只有一个线程重建缓存。

互斥锁保护示例
java
1@Service
2public class MutexCacheService {
3 private final Map<String, ReentrantLock> locks = new ConcurrentHashMap<>();
4
5 public User getUser(String userId) {
6 String cacheKey = "user:" + userId;
7
8 // 先查缓存
9 User user = (User) redisTemplate.opsForValue().get(cacheKey);
10 if (user != null) {
11 return user;
12 }
13
14 // 获取该key的互斥锁
15 ReentrantLock lock = locks.computeIfAbsent(cacheKey, k -> new ReentrantLock());
16
17 try {
18 lock.lock();
19
20 // 双重检查,防止在获取锁期间其他线程已经重建了缓存
21 user = (User) redisTemplate.opsForValue().get(cacheKey);
22 if (user != null) {
23 return user;
24 }
25
26 // 重建缓存
27 user = userRepository.findById(userId);
28 if (user != null) {
29 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
30 }
31
32 return user;
33 } finally {
34 lock.unlock();
35 }
36 }
37}

SingleFlight模式

使用SingleFlight模式,确保同一时间只有一个请求重建缓存,其他请求等待结果。

SingleFlight模式示例
java
1@Service
2public class SingleFlightCacheService {
3 private final Map<String, CompletableFuture<User>> flightMap = new ConcurrentHashMap<>();
4
5 public User getUser(String userId) {
6 String cacheKey = "user:" + userId;
7
8 // 先查缓存
9 User user = (User) redisTemplate.opsForValue().get(cacheKey);
10 if (user != null) {
11 return user;
12 }
13
14 // 检查是否已有重建任务在进行
15 CompletableFuture<User> future = flightMap.get(cacheKey);
16 if (future != null) {
17 try {
18 return future.get(5, TimeUnit.SECONDS); // 等待结果
19 } catch (Exception e) {
20 // 超时或异常,移除future并重新尝试
21 flightMap.remove(cacheKey);
22 }
23 }
24
25 // 创建新的重建任务
26 future = CompletableFuture.supplyAsync(() -> {
27 try {
28 User loadedUser = userRepository.findById(userId);
29 if (loadedUser != null) {
30 redisTemplate.opsForValue().set(cacheKey, loadedUser, 30, TimeUnit.MINUTES);
31 }
32 return loadedUser;
33 } finally {
34 // 任务完成后移除future
35 flightMap.remove(cacheKey);
36 }
37 });
38
39 flightMap.put(cacheKey, future);
40
41 try {
42 return future.get(5, TimeUnit.SECONDS);
43 } catch (Exception e) {
44 flightMap.remove(cacheKey);
45 throw new RuntimeException("Failed to load user", e);
46 }
47 }
48}

2.4 缓存一致性策略

失效策略(Cache-Aside)

写操作时删除缓存,读操作时重建缓存。

失效策略示例
java
1@Service
2public class InvalidationStrategy {
3
4 public void updateUser(User user) {
5 // 1. 更新数据库
6 userRepository.save(user);
7
8 // 2. 删除缓存
9 String cacheKey = "user:" + user.getId();
10 redisTemplate.delete(cacheKey);
11
12 // 3. 可选:延迟双删,避免并发问题
13 scheduleDelayedDelete(cacheKey, 100); // 100ms后再次删除
14 }
15
16 private void scheduleDelayedDelete(String key, long delayMs) {
17 CompletableFuture.runAsync(() -> {
18 try {
19 Thread.sleep(delayMs);
20 redisTemplate.delete(key);
21 } catch (InterruptedException e) {
22 Thread.currentThread().interrupt();
23 }
24 });
25 }
26}

更新策略

写操作时同时更新缓存和数据库。

更新策略示例
java
1@Service
2public class UpdateStrategy {
3
4 @Transactional
5 public void updateUser(User user) {
6 // 1. 更新数据库
7 userRepository.save(user);
8
9 // 2. 更新缓存
10 String cacheKey = "user:" + user.getId();
11 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
12 }
13
14 // 使用分布式锁保证更新原子性
15 public void updateUserWithLock(User user) {
16 String lockKey = "lock:user:" + user.getId();
17 String cacheKey = "user:" + user.getId();
18
19 try {
20 // 获取分布式锁
21 if (acquireLock(lockKey, 10, TimeUnit.SECONDS)) {
22 // 更新数据库
23 userRepository.save(user);
24
25 // 更新缓存
26 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
27 }
28 } finally {
29 // 释放锁
30 releaseLock(lockKey);
31 }
32 }
33}
yaml
1# Redis淘汰策略(redis.conf)
2maxmemory 1gb
3maxmemory-policy allkeys-lru

缓存一致性

  • 最终一致:写库后删缓存(或更新缓存),允许短暂不一致
  • 强一致:写入链路串行化(先删后写 + 同步更新),成本高
  • 订阅通知:通过发布订阅/流通知各副本失效

双删示例:

java
1public void updateThenEvict(User user) {
2 // 先更新DB
3 userRepository.save(user);
4 // 删缓存
5 redisTemplate.delete("user:" + user.getId());
6 // 延迟双删,避免并发读写穿透旧值
7 java.util.concurrent.CompletableFuture.runAsync(() -> {
8 try { Thread.sleep(300); } catch (InterruptedException ignored) {}
9 redisTemplate.delete("user:" + user.getId());
10 });
11}

3. 缓存问题与解决方案

3.1 缓存穿透(Cache Penetration)

缓存穿透是指查询一个不存在的数据,由于缓存中没有,每次请求都会打到数据库。

问题分析

解决方案

1. 布隆过滤器

布隆过滤器解决方案
java
1@Service
2public class BloomFilterCacheService {
3 private final BloomFilter<String> bloomFilter;
4 private final RedisTemplate<String, Object> redisTemplate;
5
6 public BloomFilterCacheService() {
7 // 创建布隆过滤器,预期元素数量100万,误判率0.01
8 this.bloomFilter = BloomFilter.create(
9 Funnels.stringFunnel(Charset.defaultCharset()),
10 1_000_000,
11 0.01
12 );
13 }
14
15 public User getUser(String userId) {
16 // 1. 布隆过滤器检查
17 if (!bloomFilter.mightContain(userId)) {
18 return null; // 肯定不存在
19 }
20
21 // 2. 查询缓存
22 String cacheKey = "user:" + userId;
23 User user = (User) redisTemplate.opsForValue().get(cacheKey);
24 if (user != null) {
25 return user;
26 }
27
28 // 3. 查询数据库
29 user = userRepository.findById(userId);
30
31 if (user != null) {
32 // 4. 更新缓存和布隆过滤器
33 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
34 bloomFilter.put(userId);
35 } else {
36 // 5. 缓存空值,防止穿透
37 redisTemplate.opsForValue().set(cacheKey, null, 5, TimeUnit.MINUTES);
38 }
39
40 return user;
41 }
42}

2. 缓存空值

缓存空值解决方案
java
1@Service
2public class NullValueCacheService {
3
4 public User getUser(String userId) {
5 String cacheKey = "user:" + userId;
6
7 // 查询缓存
8 Object cached = redisTemplate.opsForValue().get(cacheKey);
9 if (cached != null) {
10 // 检查是否是空值标记
11 if (cached instanceof NullValue) {
12 return null;
13 }
14 return (User) cached;
15 }
16
17 // 查询数据库
18 User user = userRepository.findById(userId);
19
20 if (user != null) {
21 // 缓存用户数据
22 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
23 } else {
24 // 缓存空值标记,短TTL
25 redisTemplate.opsForValue().set(cacheKey, new NullValue(), 5, TimeUnit.MINUTES);
26 }
27
28 return user;
29 }
30
31 // 空值标记类
32 private static class NullValue {
33 // 用于标识空值的标记类
34 }
35}

3.2 缓存击穿(Cache Breakdown)

缓存击穿是指热点key在缓存过期的一瞬间,大量并发请求直接打到数据库。

问题分析

解决方案

1. 互斥锁保护

互斥锁解决方案
java
1@Service
2public class MutexLockCacheService {
3 private final Map<String, ReentrantLock> locks = new ConcurrentHashMap<>();
4
5 public User getHotUser(String userId) {
6 String cacheKey = "user:" + userId;
7
8 // 先查缓存
9 User user = (User) redisTemplate.opsForValue().get(cacheKey);
10 if (user != null) {
11 return user;
12 }
13
14 // 获取该key的互斥锁
15 ReentrantLock lock = locks.computeIfAbsent(cacheKey, k -> new ReentrantLock());
16
17 try {
18 // 尝试获取锁,设置超时时间
19 if (lock.tryLock(3, TimeUnit.SECONDS)) {
20 try {
21 // 双重检查
22 user = (User) redisTemplate.opsForValue().get(cacheKey);
23 if (user != null) {
24 return user;
25 }
26
27 // 重建缓存
28 user = userRepository.findById(userId);
29 if (user != null) {
30 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
31 }
32
33 return user;
34 } finally {
35 lock.unlock();
36 }
37 } else {
38 // 获取锁失败,等待一段时间后重试
39 Thread.sleep(100);
40 return getHotUser(userId);
41 }
42 } catch (InterruptedException e) {
43 Thread.currentThread().interrupt();
44 throw new RuntimeException("Interrupted while waiting for lock", e);
45 }
46 }
47}

2. 逻辑永不过期

逻辑永不过期解决方案
java
1@Service
2public class LogicalExpirationCacheService {
3
4 public User getHotUser(String userId) {
5 String cacheKey = "user:" + userId;
6
7 // 查询缓存
8 CacheEntry<User> entry = (CacheEntry<User>) redisTemplate.opsForValue().get(cacheKey);
9
10 if (entry != null && !entry.isExpired()) {
11 return entry.getData();
12 }
13
14 // 缓存过期或不存在,异步重建
15 if (entry == null || entry.isExpired()) {
16 CompletableFuture.runAsync(() -> {
17 try {
18 User user = userRepository.findById(userId);
19 if (user != null) {
20 // 设置逻辑过期时间
21 CacheEntry<User> newEntry = new CacheEntry<>(user,
22 System.currentTimeMillis() + 30 * 60 * 1000); // 30分钟后逻辑过期
23 redisTemplate.opsForValue().set(cacheKey, newEntry);
24 }
25 } catch (Exception e) {
26 log.error("Failed to rebuild cache for user: " + userId, e);
27 }
28 });
29 }
30
31 // 返回旧数据(如果存在)
32 return entry != null ? entry.getData() : null;
33 }
34
35 // 缓存条目类
36 private static class CacheEntry<T> {
37 private final T data;
38 private final long expireTime;
39
40 public CacheEntry(T data, long expireTime) {
41 this.data = data;
42 this.expireTime = expireTime;
43 }
44
45 public T getData() { return data; }
46 public boolean isExpired() { return System.currentTimeMillis() > expireTime; }
47 }
48}

3.3 缓存雪崩(Cache Avalanche)

缓存雪崩是指大量缓存同时过期,导致大量请求直接打到数据库。

问题分析

解决方案

1. TTL随机抖动

TTL随机抖动解决方案
java
1public class RandomTTLStrategy {
2
3 public static Duration getRandomizedTTL(Duration baseTTL) {
4 long baseSeconds = baseTTL.getSeconds();
5 // 在基础TTL基础上增加随机偏移,避免同时过期
6 long randomOffset = (long) (baseSeconds * 0.2 * Math.random()); // 20%随机偏移
7 return Duration.ofSeconds(baseSeconds + randomOffset);
8 }
9
10 public void setCacheWithRandomTTL(String key, Object value) {
11 Duration ttl = getRandomizedTTL(Duration.ofMinutes(30));
12 redisTemplate.opsForValue().set(key, value, ttl);
13 }
14}

2. 缓存预热

缓存预热解决方案
java
1@Service
2public class CacheWarmupService {
3
4 @PostConstruct
5 public void warmupCache() {
6 // 系统启动时预热热点数据
7 List<String> hotUserIds = getHotUserIds();
8
9 for (String userId : hotUserIds) {
10 CompletableFuture.runAsync(() -> {
11 try {
12 User user = userRepository.findById(userId);
13 if (user != null) {
14 String cacheKey = "user:" + userId;
15 Duration ttl = getRandomizedTTL(Duration.ofMinutes(30));
16 redisTemplate.opsForValue().set(cacheKey, user, ttl);
17 }
18 } catch (Exception e) {
19 log.error("Failed to warmup cache for user: " + userId, e);
20 }
21 });
22 }
23 }
24
25 private List<String> getHotUserIds() {
26 // 获取热点用户ID列表
27 return Arrays.asList("user1", "user2", "user3", "user4", "user5");
28 }
29}

3. 熔断降级

熔断降级解决方案
java
1@Service
2public class CircuitBreakerCacheService {
3 private final CircuitBreaker circuitBreaker;
4
5 public CircuitBreakerCacheService() {
6 this.circuitBreaker = CircuitBreaker.builder()
7 .failureRateThreshold(50) // 失败率阈值50%
8 .waitDurationInOpenState(Duration.ofSeconds(10)) // 熔断时间10秒
9 .ringBufferSizeInHalfOpenState(2) // 半开状态下的请求数
10 .ringBufferSizeInClosedState(10) // 关闭状态下的请求数
11 .build();
12 }
13
14 public User getUser(String userId) {
15 return circuitBreaker.executeSupplier(() -> {
16 String cacheKey = "user:" + userId;
17
18 // 先查缓存
19 User user = (User) redisTemplate.opsForValue().get(cacheKey);
20 if (user != null) {
21 return user;
22 }
23
24 // 缓存未命中,查询数据库
25 user = userRepository.findById(userId);
26 if (user != null) {
27 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
28 }
29
30 return user;
31 });
32 }
33}

逻辑永不过期:

java
1class CacheEntry`<T>` { T data; long expireAt; }
2public CacheEntry`<User>` getUser(long id) {
3 CacheEntry`<User>` entry = localCache.get(id);
4 if (entry != null && System.currentTimeMillis() < entry.expireAt) {
5 return entry; // 命中新鲜数据
6 }
7 // 过期则触发后台刷新,但继续返回旧值(若存在)
8 backgroundRefresh(id);
9 return entry; // 旧值容忍
10}

4. 缓存监控与运维

4.1 关键监控指标

性能指标

  • 命中率(Hit Rate):缓存命中次数 / 总请求次数
  • 平均延迟(Average Latency):缓存操作的平均响应时间
  • P99延迟:99%请求的响应时间
  • 失败率(Error Rate):缓存操作失败的比例

容量指标

  • 内存使用率:缓存占用的内存比例
  • 连接数:当前活跃的客户端连接数
  • 带宽使用:网络传输的数据量

业务指标

  • 热点Key排行:访问频率最高的缓存键
  • 慢查询:响应时间超过阈值的查询
  • 大Key:占用内存较大的缓存项
缓存监控示例
java
1@Service
2public class CacheMonitorService {
3
4 @EventListener
5 public void onCacheEvent(CacheEvent event) {
6 // 记录缓存事件
7 recordCacheEvent(event);
8
9 // 更新监控指标
10 updateMetrics(event);
11 }
12
13 private void recordCacheEvent(CacheEvent event) {
14 // 记录缓存命中/未命中
15 if (event.getType() == CacheEventType.HIT) {
16 hitCounter.increment();
17 } else if (event.getType() == CacheEventType.MISS) {
18 missCounter.increment();
19 }
20
21 // 记录响应时间
22 latencyHistogram.record(event.getLatency());
23 }
24
25 private void updateMetrics(CacheEvent event) {
26 // 更新热点Key统计
27 hotKeyCounter.increment(event.getKey());
28
29 // 检查大Key
30 if (event.getValueSize() > 1024 * 1024) { // 1MB
31 bigKeyAlert.alert(event.getKey(), event.getValueSize());
32 }
33 }
34
35 // 获取缓存命中率
36 public double getHitRate() {
37 long hits = hitCounter.get();
38 long misses = missCounter.get();
39 long total = hits + misses;
40 return total > 0 ? (double) hits / total : 0.0;
41 }
42
43 // 获取P99延迟
44 public long getP99Latency() {
45 return latencyHistogram.getSnapshot().get99thPercentile();
46 }
47}

4.2 告警策略

缓存告警示例
java
1@Component
2public class CacheAlertService {
3
4 @Scheduled(fixedRate = 60000) // 每分钟检查一次
5 public void checkCacheHealth() {
6 // 检查命中率
7 double hitRate = cacheMonitorService.getHitRate();
8 if (hitRate < 0.8) { // 命中率低于80%
9 alertService.sendAlert("Cache hit rate is low: " + hitRate);
10 }
11
12 // 检查延迟
13 long p99Latency = cacheMonitorService.getP99Latency();
14 if (p99Latency > 100) { // P99延迟超过100ms
15 alertService.sendAlert("Cache P99 latency is high: " + p99Latency + "ms");
16 }
17
18 // 检查内存使用率
19 double memoryUsage = cacheMonitorService.getMemoryUsage();
20 if (memoryUsage > 0.9) { // 内存使用率超过90%
21 alertService.sendAlert("Cache memory usage is high: " + memoryUsage);
22 }
23 }
24}

4.3 运维最佳实践

容量规划

容量规划示例
java
1public class CapacityPlanningService {
2
3 public CacheConfig calculateOptimalConfig(int expectedQPS, int dataSize) {
4 // 根据预期QPS和数据大小计算最优配置
5 int optimalConnections = Math.max(10, expectedQPS / 1000);
6 long optimalMemory = dataSize * 2; // 预留2倍空间
7
8 return CacheConfig.builder()
9 .maxConnections(optimalConnections)
10 .maxMemory(optimalMemory)
11 .build();
12 }
13
14 public void scaleCache(int currentQPS, int targetQPS) {
15 // 根据QPS变化自动扩缩容
16 if (targetQPS > currentQPS * 1.5) {
17 // 扩容
18 scaleUp();
19 } else if (targetQPS < currentQPS * 0.5) {
20 // 缩容
21 scaleDown();
22 }
23 }
24}

5. 分布式缓存面试题精选

5.1 基础概念

Q1: 什么是分布式缓存?它的主要作用是什么?

: 分布式缓存是一种将数据存储在多个节点上的缓存系统,用于提升系统性能和可扩展性。

主要作用:

  • 性能提升:将热点数据存储在内存中,大幅提升访问速度
  • 压力缓解:减少对数据库的直接访问,降低数据库压力
  • 高可用性:通过多节点部署提高系统可用性
  • 水平扩展:支持动态添加节点来提升容量和性能

Q2: 本地缓存和分布式缓存的区别是什么?

:

特性本地缓存分布式缓存
部署位置应用进程内独立服务器
访问速度极快(内存访问)较快(网络访问)
容量有限(受JVM内存限制)大(可扩展)
一致性难以保证相对容易保证
适用场景高频访问的配置信息跨服务共享数据

5.2 缓存策略

Q3: Cache-Aside、Write-Through、Write-Behind三种缓存模式的区别是什么?

:

  1. Cache-Aside(旁路缓存)

    • 应用程序直接管理缓存
    • 读:先查缓存,未命中则查数据库并更新缓存
    • 写:先更新数据库,再删除缓存
    • 优点:实现简单,缓存策略灵活
    • 缺点:需要手动管理缓存一致性
  2. Write-Through(读写穿透)

    • 缓存作为数据库的代理
    • 所有读写都经过缓存
    • 优点:缓存一致性容易保证
    • 缺点:增加了系统复杂性
  3. Write-Behind(异步写入)

    • 写操作先更新缓存,然后异步批量写入数据库
    • 优点:写入性能高,减少数据库压力
    • 缺点:数据可能丢失,一致性较弱

Q4: 如何解决缓存穿透问题?

: 缓存穿透是指查询不存在的数据,每次请求都打到数据库。

解决方案:

  1. 布隆过滤器:快速判断数据是否存在,避免无效查询
  2. 缓存空值:将空结果也缓存,设置较短的TTL
  3. 参数校验:在应用层进行参数校验,过滤无效请求
  4. 限流措施:对异常请求进行限流
布隆过滤器解决方案
java
1public User getUser(String userId) {
2 // 1. 布隆过滤器检查
3 if (!bloomFilter.mightContain(userId)) {
4 return null; // 肯定不存在
5 }
6
7 // 2. 查询缓存
8 User user = (User) redisTemplate.opsForValue().get("user:" + userId);
9 if (user != null) {
10 return user;
11 }
12
13 // 3. 查询数据库
14 user = userRepository.findById(userId);
15 if (user != null) {
16 redisTemplate.opsForValue().set("user:" + userId, user, 30, TimeUnit.MINUTES);
17 } else {
18 // 缓存空值,防止穿透
19 redisTemplate.opsForValue().set("user:" + userId, null, 5, TimeUnit.MINUTES);
20 }
21
22 return user;
23}

5.3 缓存问题

Q5: 如何解决缓存击穿问题?

: 缓存击穿是指热点key在缓存过期的一瞬间,大量并发请求直接打到数据库。

解决方案:

  1. 互斥锁:确保同一时间只有一个线程重建缓存
  2. SingleFlight模式:确保同一时间只有一个请求重建缓存,其他请求等待结果
  3. 逻辑永不过期:设置逻辑过期时间,异步重建缓存
  4. 预热机制:在缓存过期前主动重建缓存
互斥锁解决方案
java
1public User getHotUser(String userId) {
2 String cacheKey = "user:" + userId;
3
4 // 先查缓存
5 User user = (User) redisTemplate.opsForValue().get(cacheKey);
6 if (user != null) {
7 return user;
8 }
9
10 // 获取该key的互斥锁
11 ReentrantLock lock = locks.computeIfAbsent(cacheKey, k -> new ReentrantLock());
12
13 try {
14 lock.lock();
15
16 // 双重检查
17 user = (User) redisTemplate.opsForValue().get(cacheKey);
18 if (user != null) {
19 return user;
20 }
21
22 // 重建缓存
23 user = userRepository.findById(userId);
24 if (user != null) {
25 redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
26 }
27
28 return user;
29 } finally {
30 lock.unlock();
31 }
32}

Q6: 如何解决缓存雪崩问题?

: 缓存雪崩是指大量缓存同时过期,导致大量请求直接打到数据库。

解决方案:

  1. TTL随机抖动:在基础TTL基础上增加随机偏移,避免同时过期
  2. 缓存预热:系统启动时主动加载热点数据到缓存
  3. 熔断降级:当数据库压力过大时,启用熔断机制
  4. 多级缓存:使用本地缓存作为最后一道防线
TTL随机抖动解决方案
java
1public static Duration getRandomizedTTL(Duration baseTTL) {
2 long baseSeconds = baseTTL.getSeconds();
3 // 在基础TTL基础上增加随机偏移,避免同时过期
4 long randomOffset = (long) (baseSeconds * 0.2 * Math.random()); // 20%随机偏移
5 return Duration.ofSeconds(baseSeconds + randomOffset);
6}

5.4 高级问题

Q7: 双删策略为什么要延迟?延迟多长时间合适?

: 双删策略是为了解决缓存一致性问题,延迟删除是为了避免并发问题。

原因:

  1. 并发问题:在删除缓存和更新数据库之间,可能有其他线程读取到旧数据并更新缓存
  2. 时序问题:延迟删除可以确保在并发操作完成后再次清理缓存

延迟时间

  • 一般设置为100-500ms
  • 需要根据业务场景和系统负载调整
  • 可以通过监控和测试确定最优值
延迟双删示例
java
1public void updateUser(User user) {
2 // 1. 更新数据库
3 userRepository.save(user);
4
5 // 2. 删除缓存
6 String cacheKey = "user:" + user.getId();
7 redisTemplate.delete(cacheKey);
8
9 // 3. 延迟双删,避免并发问题
10 scheduleDelayedDelete(cacheKey, 100); // 100ms后再次删除
11}
12
13private void scheduleDelayedDelete(String key, long delayMs) {
14 CompletableFuture.runAsync(() -> {
15 try {
16 Thread.sleep(delayMs);
17 redisTemplate.delete(key);
18 } catch (InterruptedException e) {
19 Thread.currentThread().interrupt();
20 }
21 });
22}

Q8: 多级缓存一致性如何保证?

: 多级缓存一致性是一个复杂的问题,需要综合考虑性能和一致性。

解决方案:

  1. 失效策略:写操作时同时失效所有级别的缓存
  2. 版本控制:使用版本号控制缓存更新
  3. 订阅通知:通过消息队列通知各节点更新缓存
  4. 最终一致性:接受短暂不一致,通过TTL保证最终一致性
多级缓存一致性示例
java
1@Service
2public class MultiLevelCacheService {
3 private final Cache<String, Object> localCache;
4 private final RedisTemplate<String, Object> redisCache;
5
6 public void updateUser(User user) {
7 // 1. 更新数据库
8 userRepository.save(user);
9
10 // 2. 同时失效两级缓存
11 String cacheKey = "user:" + user.getId();
12 localCache.invalidate(cacheKey);
13 redisCache.delete(cacheKey);
14
15 // 3. 发送消息通知其他节点
16 messagePublisher.publish("cache:invalidate", cacheKey);
17 }
18
19 @EventListener
20 public void onCacheInvalidate(String cacheKey) {
21 // 收到失效消息时,清理本地缓存
22 localCache.invalidate(cacheKey);
23 }
24}

Q9: 如何识别与治理热点Key?

: 热点Key是指访问频率异常高的缓存键,需要特殊处理。

识别方法:

  1. 监控统计:通过监控系统统计Key的访问频率
  2. 实时告警:设置阈值,当Key访问频率超过阈值时告警
  3. 日志分析:分析访问日志,识别热点Key

治理策略:

  1. 本地缓存:将热点Key缓存到本地,减少网络开销
  2. 分片策略:将热点Key分散到多个节点
  3. 预加载:主动预加载热点数据
  4. 降级策略:当热点Key压力过大时,启用降级策略
热点Key治理示例
java
1@Service
2public class HotKeyService {
3
4 @Scheduled(fixedRate = 60000) // 每分钟检查一次
5 public void detectHotKeys() {
6 Map<String, Long> keyAccessCount = getKeyAccessCount();
7
8 for (Map.Entry<String, Long> entry : keyAccessCount.entrySet()) {
9 if (entry.getValue() > 1000) { // 访问次数超过1000
10 String hotKey = entry.getKey();
11 handleHotKey(hotKey);
12 }
13 }
14 }
15
16 private void handleHotKey(String hotKey) {
17 // 1. 预加载到本地缓存
18 Object value = redisTemplate.opsForValue().get(hotKey);
19 if (value != null) {
20 localCache.put(hotKey, value);
21 }
22
23 // 2. 发送告警
24 alertService.sendAlert("Hot key detected: " + hotKey);
25
26 // 3. 记录日志
27 log.warn("Hot key detected: {} with access count: {}", hotKey, getAccessCount(hotKey));
28 }
29}

5.5 性能优化

Q10: 如何优化缓存性能?

: 缓存性能优化需要从多个维度考虑。

优化策略:

  1. 键设计优化

    • 使用短而清晰的键名
    • 避免过长的键名
    • 使用压缩算法减少键长度
  2. 数据结构优化

    • 选择合适的Redis数据结构
    • 避免存储冗余数据
    • 使用序列化优化
  3. 网络优化

    • 使用连接池
    • 批量操作减少网络往返
    • 使用Pipeline减少网络延迟
  4. 内存优化

    • 合理设置TTL
    • 使用LRU等淘汰策略
    • 监控内存使用情况
性能优化示例
java
1@Service
2public class OptimizedCacheService {
3
4 // 使用连接池
5 private final RedisTemplate<String, Object> redisTemplate;
6
7 // 批量操作
8 public Map<String, Object> batchGet(List<String> keys) {
9 return redisTemplate.opsForValue().multiGet(keys);
10 }
11
12 // Pipeline操作
13 public void batchSet(Map<String, Object> data) {
14 redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
15 for (Map.Entry<String, Object> entry : data.entrySet()) {
16 connection.set(
17 entry.getKey().getBytes(),
18 serialize(entry.getValue())
19 );
20 }
21 return null;
22 });
23 }
24
25 // 压缩键名
26 public String compressKey(String originalKey) {
27 return String.valueOf(originalKey.hashCode());
28 }
29}

6. 总结

分布式缓存是现代互联网应用的核心组件,通过合理的设计和优化,可以显著提升系统性能。在实际应用中,需要根据业务场景选择合适的缓存策略,并注意解决缓存穿透、击穿、雪崩等问题。

关键要点

  1. 选择合适的缓存模式:根据业务需求选择Cache-Aside、Write-Through或Write-Behind
  2. 解决缓存问题:使用布隆过滤器、互斥锁、TTL随机抖动等技术
  3. 保证缓存一致性:通过失效策略、版本控制等方式保证数据一致性
  4. 监控和优化:建立完善的监控体系,持续优化缓存性能

通过深入理解和熟练运用这些技术,我们能够构建出高性能、高可用的分布式缓存系统。

参与讨论