缓存-服务器缓存
本地缓存
本地缓存可以在减少对缓存服务的访问量,降低访问带来的时延,提升性能.同时也会带来一些问题,比如本地缓存与缓存服务数据一致性问题,以及如果命中率过低或刷新缓存过于频繁或本地缓存缓存数量过大(超过热点内容数量)可能会导致回源流量过大.
以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 { Cache<String, String> loadingCache = Caffeine.newBuilder() .initialCapacity(5) .maximumSize(10) .expireAfterWrite(17, TimeUnit.SECONDS) .build(); String key = "key"; loadingCache.put(key, "v"); String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB); 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<Key, Graph> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10_000) .build();
Graph graph = cache.getIfPresent(key);
graph = cache.get(key, k -> createExpensiveGraph(key));
cache.put(key, graph);
cache.invalidate(key);
LoadingCache<Key, Graph> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(key -> createExpensiveGraph(key));
Graph graph = cache.get(key);
Map<Key, Graph> graphs = cache.getAll(keys);
AsyncCache<Key, Graph> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10_000) .buildAsync();
CompletableFuture<Graph> graph = cache.getIfPresent(key);
graph = cache.get(key, k -> createExpensiveGraph(key));
cache.put(key, graph);
cache.synchronous().invalidate(key);
AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(10, TimeUnit.MINUTES) .buildAsync(key -> createExpensiveGraph(key)); .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) { 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));
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder() .weakKeys() .weakValues() .build(key -> createExpensiveGraph(key));
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请求,大量缓存回源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