缓存-服务器缓存

本地缓存

本地缓存可以在减少对缓存服务的访问量,降低访问带来的时延,提升性能.同时也会带来一些问题,比如本地缓存与缓存服务数据一致性问题,以及如果命中率过低或刷新缓存过于频繁或本地缓存缓存数量过大(超过热点内容数量)可能会导致回源流量过大.

以Caffeine为例,Caffeine采用了W-TinyLFU(LUR和LFU的优点结合)开源的缓存技术.

<dependency>  
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
public class CaffeineCacheTest {

public static void main(String[] args) throws Exception {
//创建guava cache
Cache<String, String> loadingCache = Caffeine.newBuilder()
//cache的初始容量
.initialCapacity(5)
//cache最大缓存数
.maximumSize(10)
//设置写缓存后n秒钟过期
.expireAfterWrite(17, TimeUnit.SECONDS)
//设置读写缓存后n秒钟过期,类似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
.build();
String key = "key";
// 往缓存写数据
loadingCache.put(key, "v");
// 获取value的值,如果key不存在,获取value后再返回
String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB);
// 删除key
loadingCache.invalidate(key);
}

private static String getValueFromDB(String key) {
return "v";
}
}
@Configuration
public class CacheConfig {

@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
}
}

缓存类型

实习的时候组里用的一般都是异步回源缓存,下面是redis/memcache,本地缓存降低缓存服务访问,减少带宽消耗.

// Cache
Cache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
// 查找一个缓存元素, 没有查找到的时候返回null
Graph graph = cache.getIfPresent(key);
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.invalidate(key);


// Loading Cache
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));

// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回null
Graph graph = cache.get(key);
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<Key, Graph> graphs = cache.getAll(keys);


// Async Cache
AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10_000)
.buildAsync();
// 查找一个缓存元素, 没有查找到的时候返回null
CompletableFuture<Graph> graph = cache.getIfPresent(key);
// 查找缓存元素,如果不存在,则异步生成
graph = cache.get(key, k -> createExpensiveGraph(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.synchronous().invalidate(key);


//Async Loading Cache
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
// 可以选择: 去异步的封装一段同步操作来生成缓存元素
.buildAsync(key -> createExpensiveGraph(key));
// 也可以选择: 构建一个异步缓存元素操作并返回一个future
.buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));
// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Graph> graph = cache.get(key);
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

淘汰策略

//基于容量


// 基于缓存内的元素个数进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build(key -> createExpensiveGraph(key));

// 基于缓存内元素权重进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.weigher((Key key, Graph graph) -> graph.vertices().size())
.build(key -> createExpensiveGraph(key));

//基于时间


// 基于固定的过期时间驱逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(key -> createExpensiveGraph(key));

// 基于不同的过期驱逐策略
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.expireAfter(new Expiry<Key, Graph>() {
public long expireAfterCreate(Key key, Graph graph, long currentTime) {
// Use wall clock time, rather than nanotime, if from an external resource
long seconds = graph.creationDate().plusHours(5)
.minus(System.currentTimeMillis(), MILLIS)
.toEpochSecond();
return TimeUnit.SECONDS.toNanos(seconds);
}
public long expireAfterUpdate(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
public long expireAfterRead(Key key, Graph graph,
long currentTime, long currentDuration) {
return currentDuration;
}
})
.build(key -> createExpensiveGraph(key));


//基于引用


// 当key和缓存元素都不再存在其他强引用的时候驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.weakKeys()
.weakValues()
.build(key -> createExpensiveGraph(key));

// 当进行GC的时候进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.softValues()
.build(key -> createExpensiveGraph(key));

之前使用本地缓存遇到过几个坑

  • 本地缓存和redis数据不一致, 本地缓存设置的是5s刷一次,从redis中回源,遇到的场景是MQ消费消息时,从本地缓存读取一个配置,当时遇到的问题是两个配置不一致,修复策略是设置10s(>5s)的延迟消费.
  • 一个需求从其他服务读取一个信息,这个信息为热点信息且更新不频繁,故引入本地缓存,设置定时刷新和定时定时淘汰,但是调用下游的qps和存储信息数量接近,命中率过低,后面通过调整数量大小和回源时间降低回源qps,提高服务可用度.

缓存中间件

Redis

  • 主从/哨兵/集群实现分布式
  • 使用时候需要关注大key问题
  • 需要关注数据淘汰问题
  • 慢查询问题

Memcache

  • 分片路由实现分布式
  • 性能更高

缓存穿透

大量无效key请求,导致大量缓存回源DB,击溃服务

  • 缓存无效key
  • 布隆过滤器

缓存击穿

大量过期key请求,大量缓存回源DB,击溃服务,本质和缓存穿透差不多

  • 设置多级缓存,容灾缓存
  • 服务上线前,数据预热
  • 回源db时,设置幂等锁
  • 随机过期时间

缓存雪崩

大量有效请求,回源DB,击溃服务

  • 扩容集群
  • 限流
  • 设置降级策略(要考虑降级恢复,腾讯视频前阵子的会员服务崩溃疑似就是长期没有恢复)
  • 多级缓存
  • 随机过期时间
  • 回源db时,设置幂等锁

缓存一致性问题

  • 延迟双删
  • 设置一个cacheSetter服务,回源和更新统一由cacheSetter服务控制,回源由服务设置,消费MySQL binlog更新cacheSetter同时也可以更新缓存

参考文献

https://jaskey.github.io/blog/2022/04/14/cache-consistency/
https://www.yuucn.com/a/124328.html
https://zhuanlan.zhihu.com/p/496696480?utm_id=0
https://zhuanlan.zhihu.com/p/347246715
https://zhuanlan.zhihu.com/p/608510846