美文网首页
java 关于内存缓存

java 关于内存缓存

作者: 瓢鳍小虾虎 | 来源:发表于2021-01-09 12:21 被阅读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)定时同步:适用一些实时性不高且计算耗时的任务,如统计报表、订单跟踪等。一版通过一些定时任务来实现。
      例如:
    • ScheduledExecutorService
    • Spring Task定时任务注解
    @Scheduled(cron="0 0 0/1 * * ?")
    
    • 定时任务框架如Quartz

      1. binlog日志订阅:主要是指用一个服务订阅mysql的binlog,从形式上这个服务是作为mysql的slave。binlog的优点在于在压力不大的时候性能比较好,与业务完全解耦。

    处理缓存同步问题目前比较常用的方案是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
    常用缓存淘汰算法

    相关文章

      网友评论

          本文标题:java 关于内存缓存

          本文链接:https://www.haomeiwen.com/subject/dbbzoktx.html