缓存的使用目的是为了提高处理速度,减轻服务器压力,提高用户体验。
缓存的本质是一种用空间换时间的策略。
常见的缓存有:
1)硬件缓存
2)客户端缓存
3)服务端缓存
后两种属于缓存的技术实现,甚至可以表现为一个服务。
缓存的特点:
1)可以设置过期时间
2)空间占用有限:缓存超过上限的时候需要根据淘汰策略(FIFO先进先出、LRU最近使用优先、LFU频率最高优先)提出旧的缓存
3)支持并发读写
使用缓存常见的问题:
-
缓存穿透:指的是用户查询的信息服务器根本都没有,导致无法缓存,用户每次请求都要访问一次数据库。(查不到)
-
缓存击穿:由于缓存有时效,在缓存时效的时候,用户的访问并没有停止,依然有大量该失效数据的请求,这就造成了大量请求穿过缓存直接访问数据库。(失效了还查)
-
缓存雪崩:大量的缓存在同一短暂时间时效,导致大量请求穿过缓存直接访问了数据库。(大量同时失效)
常见缓存实现方式:
- java容器:使用JDK自带的Map容器类,如HashMap、ConcurrentHashMap。
- Guava cache:Google提供的java增强工具包Guava的一个模块,目前社区活跃。
- Ehcache:重量级缓存框架,支持2级缓存,hibernate默认缓存框架。
- caffeine:基于Guava api封装的高性能内存缓存框架,Spring5开始默认内存缓存框架。目前相对主流。
使用java容器实现简易FIFO缓存:
// 使用LinkedHashMap实现FIFO缓存,LinkedHashMap本身是线程不安全的,我们利用LinkedHashMap的基础功能封装一个线程安全的缓存。
class FIFOCacheProvider {
private Map<String, CacheItem> cacheMap = null;
private final static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(5);
// 最大容量
private static int MAX_CACHE_SIZE = 0;
// 缓存因子
private final float LOAD_FACTORY = 0.75f;
public FIFOCacheProvider(int maxSize) {
MAX_CACHE_SIZE = maxSize;
// 根据缓存因子计算内部linkedHashMap容量
int capacity = (int) Math.ceil(MAX_CACHE_SIZE / LOAD_FACTORY) + 1;
// 根据容量和缓存因子初始化LinkedHashMap
cacheMap = new LinkedHashMap<String, CacheItem>(capacity, LOAD_FACTORY, false) {
// 重写剔除策略
@Override
protected boolean removeEldestEntry(Map.Entry<String, CacheItem> eldest) {
return size() > MAX_CACHE_SIZE;
}
};
}
// 重写toString
@Override
public String toString() {
// StringBuilder 相比StringBuffer 是线程安全的
StringBuilder sb = new StringBuilder();
// 循环内部map
for(Map.Entry<String, CacheItem> entry : cacheMap.entrySet()) {
sb.append(" item: key = ").append(entry.getKey()).append(" value = ").append(entry.getValue().getData()).append("\t");
}
return sb.toString();
}
// 获取
public synchronized <T> T get(String key) {
CacheItem item = cacheMap.get(key);
return item == null ? null : (T) item.getData();
}
// 删除
public synchronized <T> T remove(String key) {
CacheItem item = cacheMap.remove(key);
return item == null ? null : (T) item.getData();
}
// 总数
public synchronized int size() {
return cacheMap.size();
}
// 新增
public synchronized void put (String key, Object value) {
this.put(key, value, -1L);
}
public synchronized void put (String key, Object value, Long expire) {
cacheMap.remove(key);
if (expire > 0) {
executor.schedule(new Runnable() {
@Override
public void run() {
synchronized (this) {
cacheMap.remove(key);
}
}
}, expire, TimeUnit.MILLISECONDS);
cacheMap.put(key, new CacheItem(value, expire));
} else {
cacheMap.put(key, new CacheItem(value, -1L));
}
}
}
// 缓存测试
@Data
@Builder
class CacheItem {
private Object data;
private Long expire;
}
image.png
image.png
caffeine:
caffeine是google基于java8对GuavaCache的重写版本,特点是支持丰富的缓存过期策略,尤其是TinyLFU算法,提供了一个近乎最佳的命中率,读写效率远超于其他内存缓存框架。
caffeine的一个简单用法展示:
void testCaffeine1 () throws InterruptedException { // 手动加载
Cache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
.maximumSize(10_000)
.build();
// 默认返回值
Function<Object,Object> getFunc = key -> key + "_" + System.currentTimeMillis();
// test
String key = "key1";
Object value = cache.get(key, getFunc);
System.out.println("key:"+ value);
Thread.sleep(2001); // 让缓存过期
value = cache.getIfPresent(key); // 如果查不到会返回null
System.out.println("key:" + value);
cache.put(key, "aaa");
value = cache.get(key, getFunc);
System.out.println("key:"+value);
ConcurrentMap<String, Object> asMap = cache.asMap();
System.out.println("asMap:"+ asMap);
cache.invalidate(key);
asMap = cache.asMap();
System.out.println("asMap:" + asMap);
}
image.png
解决缓存问题方案:
- 缓存一致性问题(缓存同步问题):主要是指更新数据库的时候缓存同步的过程,如何尽量避免缓存不一致。
1)实时同步:更新数据库的时候先让缓存失效,同时为避免缓存击穿,加锁处理,保证只有一个线程在更新缓存。设置缓存失效时间,如果缓存更新失败,也可以自动失效。
2)准时同步:准时同步的相比实时同步是被动的同步,表现在数据库更新之后再处理缓存。具体表现在数据库更新后会发一个mq消息(可以准备一个本地消息表用于发送失败重发)供缓存更新服务消费,并根据数据库最新数据更新缓存。
3)定时同步:适用一些实时性不高且计算耗时的任务,如统计报表、订单跟踪等。一版通过一些定时任务来实现。
例如:
- ScheduledExecutorService
- Spring Task定时任务注解
@Scheduled(cron="0 0 0/1 * * ?")
-
定时任务框架如Quartz
- binlog日志订阅:主要是指用一个服务订阅mysql的binlog,从形式上这个服务是作为mysql的slave。binlog的优点在于在压力不大的时候性能比较好,与业务完全解耦。
处理缓存同步问题目前比较常用的方案是1)和4)。
-
缓存穿透问题:针对访问的参数不存在的情况
1)对请求参数进行合理性校验(例如id不能小于0,有某些规则等),尽量防止有人用不合理参数频繁攻击服务。
2)对查询不到的数据设置默认缓存(如null或者{}),设置较短缓存时效。
3)使用bloom filter保存缓存过的key,如果请求不存在的key则不允许访问数据库。 -
缓存击穿问题:针对热点数据缓存失效情况
1)对某些数据设置为热点数据,永远不失效。
2)更新的时候加互斥锁,热点缓存失效的时候保证只有一个线程能访问到数据库并更新缓存,其他访问的线程只能等待并重试。 -
缓存雪崩问题:主要针对某一时间点大批量缓存同时失效,同时遇到高并发访问引起数据库压力过大。
1)热点数据永不过期。
2)缓存的有效时间加随机数。
3)如果是分布式缓存,把热点数据分散在不同缓存节点上。
caffeine使用参考
https://www.jianshu.com/p/9a80c662dac4
https://zhuanlan.zhihu.com/p/329684099
https://www.jianshu.com/p/3434991ad075
常用缓存淘汰算法
网友评论