缓存-服务器缓存

本地缓存

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

​ 以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));

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

  1. 本地缓存和redis数据不一致, 本地缓存设置的是5s刷一次,从redis中回源,遇到的场景是MQ消费消息时,从本地缓存读取一个配置,当时遇到的问题是两个配置不一致,修复策略是设置10s(>5s)的延迟消费.
  2. 一个需求从其他服务读取一个信息,这个信息为热点信息且更新不频繁,故引入本地缓存,设置定时刷新和定时定时淘汰,但是调用下游的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