java 关于内存缓存

2021-01-09  本文已影响0人  瓢鳍小虾虎

缓存的使用目的是为了提高处理速度,减轻服务器压力,提高用户体验。
缓存的本质是一种用空间换时间的策略。

常见的缓存有:
1)硬件缓存
2)客户端缓存
3)服务端缓存
后两种属于缓存的技术实现,甚至可以表现为一个服务。

缓存的特点

1)可以设置过期时间

2)空间占用有限:缓存超过上限的时候需要根据淘汰策略(FIFO先进先出、LRU最近使用优先、LFU频率最高优先)提出旧的缓存

3)支持并发读写

使用缓存常见的问题

  1. 缓存穿透:指的是用户查询的信息服务器根本都没有,导致无法缓存,用户每次请求都要访问一次数据库。(查不到)

  2. 缓存击穿:由于缓存有时效,在缓存时效的时候,用户的访问并没有停止,依然有大量该失效数据的请求,这就造成了大量请求穿过缓存直接访问数据库。(失效了还查)

  3. 缓存雪崩:大量的缓存在同一短暂时间时效,导致大量请求穿过缓存直接访问了数据库。(大量同时失效)

常见缓存实现方式

  1. java容器:使用JDK自带的Map容器类,如HashMap、ConcurrentHashMap。
  2. Guava cache:Google提供的java增强工具包Guava的一个模块,目前社区活跃。
  3. Ehcache:重量级缓存框架,支持2级缓存,hibernate默认缓存框架。
  4. 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. 缓存一致性问题(缓存同步问题):主要是指更新数据库的时候缓存同步的过程,如何尽量避免缓存不一致。
    1)实时同步:更新数据库的时候先让缓存失效,同时为避免缓存击穿,加锁处理,保证只有一个线程在更新缓存。设置缓存失效时间,如果缓存更新失败,也可以自动失效。
    2)准时同步:准时同步的相比实时同步是被动的同步,表现在数据库更新之后再处理缓存。具体表现在数据库更新后会发一个mq消息(可以准备一个本地消息表用于发送失败重发)供缓存更新服务消费,并根据数据库最新数据更新缓存。
    3)定时同步:适用一些实时性不高且计算耗时的任务,如统计报表、订单跟踪等。一版通过一些定时任务来实现。
    例如:
@Scheduled(cron="0 0 0/1 * * ?")

处理缓存同步问题目前比较常用的方案是1)和4)。

  1. 缓存穿透问题:针对访问的参数不存在的情况
    1)对请求参数进行合理性校验(例如id不能小于0,有某些规则等),尽量防止有人用不合理参数频繁攻击服务。
    2)对查询不到的数据设置默认缓存(如null或者{}),设置较短缓存时效。
    3)使用bloom filter保存缓存过的key,如果请求不存在的key则不允许访问数据库。

  2. 缓存击穿问题:针对热点数据缓存失效情况
    1)对某些数据设置为热点数据,永远不失效。
    2)更新的时候加互斥锁,热点缓存失效的时候保证只有一个线程能访问到数据库并更新缓存,其他访问的线程只能等待并重试。

  3. 缓存雪崩问题:主要针对某一时间点大批量缓存同时失效,同时遇到高并发访问引起数据库压力过大。
    1)热点数据永不过期。
    2)缓存的有效时间加随机数。
    3)如果是分布式缓存,把热点数据分散在不同缓存节点上。

caffeine使用参考
https://www.jianshu.com/p/9a80c662dac4
https://zhuanlan.zhihu.com/p/329684099
https://www.jianshu.com/p/3434991ad075
常用缓存淘汰算法

上一篇下一篇

猜你喜欢

热点阅读