本地缓存的优缺点

  • 使用场景
    在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适。
  • 优点
    应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等。
  • 缺点
    缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。

本地缓存的几种实现

直接编程实现

使用静态变量实现本地缓存。

  • 使用场景:单机场景下的小数据量缓存需求,同时对缓存数据的变更无需太敏感感知,如上一般配置管理、基础静态数据等场景。
  • 优点:通过静态变量一次获取数据然后缓存在内存中,减少频繁的I/O读取,静态变量实现类间可共享,进程内可共享。
  • 缺点:缓存的实时性稍差,受java堆内存大小的影响,缓存数量有限。
  • 改进方案: 使用Zookeeper 的自动发现机制,更新缓存。

使用示例:

City.java

public class City {
    private int id;
    private String name;

    public City(int i, String name) {
        this.id = i;
        this.name = name;
    }

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public static List<City> getCitysFromDB() {
        List<City> cities = new ArrayList<>();
        cities.add(new City(1, "江西"));
        cities.add(new City(2, "上海"));
        cities.add(new City(3, "北京"));
        cities.add(new City(4, "天津"));
        cities.add(new City(5, "河北"));
        cities.add(new City(6, "湖北"));
        cities.add(new City(7, "湖南"));
        cities.add(new City(8, "广东"));
        cities.add(new City(9, "安徽"));
        cities.add(new City(10, "..."));
        return cities;
    }
}

CityLocalCacheExample.java

public class CityLocalCacheExample {
    private static Map<Integer, String> cityNameMap = new HashMap<Integer, String>();

    static {
        System.out.println("Init Cache");
        try {
            List<City> cities = City.getCitysFromDB();
            for (City city : cities) {
                cityNameMap.put(city.getId(), city.getName());
            }
        } catch (Exception e) {
            throw new RuntimeException("Init City List Error!", e);
        }
    }


    public static String getCityName(int cityId) {
        String name = cityNameMap.get(cityId);
        if (name == null) {
            name = "未知";
        }
        return name;
    }

    public static void main(String[] args) {
        String cityName = CityLocalCacheExample.getCityName(1);
        System.out.println(cityName);
    }
}

Ehcache

Ehcache是纯Java开源缓存框架,配置简单、结构清晰、功能强大,是一个非常轻量级的缓存实现.
Ehcache的核心概念主要包括:

  • cache manager:缓存管理器,以前是只允许单例的,不过现在也可以多实例了。
  • cache:缓存管理器内可以放置若干cache,存放数据的实质,所有cache都实现了Ehcache接口,这是一个真正使用的缓存实例;通过缓存管理器的模式,可以在单个应用中轻松隔离多个缓存实例,独立服务于不同业务场景需求,缓存数据物理隔离,同时需要时又可共享使用。
  • element:单条缓存数据的组成单位。

Ehcache的缓存介质涵盖堆内存(heap)、堆外内存(BigMemory商用版本支持)和磁盘,各介质可独立设置属性和策略。

  • 使用场景:大型高并发系统场景,需要快速的读取。
  • 优点:

    1. 简单,很小的jar包,简单配置就可直接使用,单机场景下无需过多的其他服务依赖。
    2. 支持多种的缓存策略,灵活。
    3. 缓存数据有两级:内存和磁盘,与一般的本地内存缓存相比,有了磁盘的存储空间,将可以支持更大量的数据缓存需求。
    4. 具有缓存和缓存管理器的侦听接口,能更简单方便的进行缓存实例的监控管理。
    5. 支持多缓存管理器实例,以及一个实例的多个缓存区域。
  • 缺点:

    1. 在使用中要注意过期失效的缓存元素无法被GC回收,时间越长缓存越多,内存占用也就越大,内存泄露的概率也越大。

使用示例:

引入Ehcache依赖

<dependency>
  <groupId>net.sf.ehcache</groupId>
  <artifactId>ehcache</artifactId>
  <version>2.10.2</version>
</dependency>

Ehcache 配置文件

<ehcache>
    <!-- 指定一个文件目录,当Ehcache把数据写到硬盘上时,将把数据写到这个文件目录下 -->
    <diskStore path="java.io.tmpdir"/>

    <!-- 设定缓存的默认数据过期策略 -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            overflowToDisk="true"
            timeToIdleSeconds="0"
            timeToLiveSeconds="0"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"/>

    <!--
        设定具体的命名缓存的数据过期策略

        cache元素的属性:
            name:缓存名称

            maxElementsInMemory:内存中最大缓存对象数

            maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大

            eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false

            overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。

            diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。

            diskPersistent:是否缓存虚拟机重启期数据

            diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒

            timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态

            timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,Ehcache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义

            memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
    -->
    <cache name="CityCache"
           maxElementsInMemory="1000"
           eternal="true"
           overflowToDisk="true"/>
</ehcache>

CityEhcacheExample.java

public class CityEhcacheExample {

    public static Cache cache = null;

    static {
        System.out.println("Init Ehcache");
        // 1. 创建缓存管理器
        CacheManager cacheManager = CacheManager.create("./src/main/resources/ehcache.xml");
        // 2. 获取缓存对象
        cache = cacheManager.getCache("CityCache");
        List<City> cities = City.getCitysFromDB();
        for(City city : cities) {
            // 3. 创建元素
            Element element = new Element(city.getId(), city.getName());
            // 4. 将元素添加到缓存
            cache.put(element);
        }
    }

    public static String getCityName(int cityId) {
        Element city = cache.get(cityId);
        if (city == null) {
            return "未知";
        }
        return (String) city.getObjectValue();
    }

    public static void main(String[] args) {
        System.out.println(getCityName(1));
    }

}

Guava Cache

Guava Cache是Google开源的Java重用工具集库Guava里的一款缓存工具,其主要实现的缓存功能有:

  • 自动将entry节点加载进缓存结构中;
  • 当缓存的数据超过设置的最大值时,使用LRU算法移除;
  • 具备根据entry节点上次被访问或者写入时间计算它的过期机制;
  • 缓存的key被封装在WeakReference引用内;
  • 缓存的Value被封装在WeakReference或SoftReference引用内;
  • 统计缓存使用过程中命中率、异常率、未命中率等统计数据。

Guava Cache继承了ConcurrentHashMap的思路,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求。Cache类似于Map,它是存储键值对的集合,不同的是它还需要处理evict、expire、dynamic load等算法逻辑,需要一些额外信息来实现这些操作。

Guava Cache提供Builder模式的CacheBuilder生成器来创建缓存的方式,十分方便,并且各个缓存参数的配置设置,类似于函数式编程的写法,可自行设置各类参数选型。它提供三种方式加载到缓存中。分别是:

  • 在构建缓存的时候,使用build方法内部调用CacheLoader方法加载数据;
  • callable 、callback方式加载数据;
  • 使用粗暴直接的方式,直接Cache.put 加载数据,但自动加载是首选的,因为它可以更容易的推断所有缓存内容的一致性。

使用示例:
引入guava cache 的依赖

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>26.0-jre</version>
</dependency>

GuavaCacheExample.java

public class GuavaCacheExample {


    private static final LoadingCache<Integer, Optional<String>> cityCache;

    static {
        cityCache = CacheBuilder.newBuilder()
                .maximumSize(1000).build(new CacheLoader<Integer, Optional<String>>() {
                    @Override
                    public Optional<String> load(Integer key) throws Exception {
                        String name = getNameById(key);
                        if (null == name) {
                            return Optional.empty();
                        } else {
                            return Optional.of(name);
                        }
                    }
                });
    }

    private static String getNameById(Integer id) {
        List<City> cities = City.getCitysFromDB();
        for(City city : cities) {
            if (city.getId() == id) {
                return city.getName();
            }
        }
        return null;
    }

    public static String getCityName(int cityId) throws ExecutionException {
        Optional<String> name =  cityCache.get(cityId);
        return name.orElse("未知");
    }


    public static void main(String[] args) throws ExecutionException {
        System.out.println(getCityName(1));
        System.out.println(getCityName(0));
    }

}

Caffeine

Caffeine是基于java8实现的新一代缓存工具,缓存性能接近理论最优。可以看作是Guava Cache的增强版,功能上两者类似,不同的是Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,在性能上有明显的优越性。
相比Guava Cache来说,Caffeine无论从功能上和性能上都有明显优势。同时两者的API类似,使用Guava Cache的代码很容易可以切换到Caffeine

引入Caffeine的依赖

<dependency>
  <groupId>com.github.ben-manes.caffeine</groupId>
  <artifactId>caffeine</artifactId>
  <version>2.5.5</version>
</dependency>

CaffeineCacheExample.java

public class CaffeineCacheExample {

    private static final Cache<Integer, String> cityCache;

    static {
        cityCache = Caffeine.newBuilder()
                .maximumSize(1000).build(new CacheLoader<Integer,String>() {
                    @Override
                    public String load(Integer key) throws Exception {
                        return getNameById(key);
                    }
                });
    }

    private static String getNameById(Integer id) {
        List<City> cities = City.getCitysFromDB();
        for(City city : cities) {
            if (city.getId() == id) {
                return city.getName();
            }
        }
        return null;
    }

    public static String getCityName(int cityId) throws ExecutionException {
       String name =  cityCache.getIfPresent(cityId);
       if (name == null) {
           name = cityCache.get(cityId, CaffeineCacheExample::getNameById);
           if (name == null) {
               return "未知";
           }
           return name;
       }
       return name;
    }

    public static void main(String[] args) throws ExecutionException {
        System.out.println(getCityName(0));
        System.out.println(getCityName(1));
    }
}

总结

从易用性角度,Guava Cache、Caffeine和Encache都有十分成熟的接入方案,使用简单。
从功能性角度,Guava Cache和Caffeine功能类似,都是只支持堆内缓存,Encache相比功能更为丰富
从性能上进行比较,Caffeine最优、GuavaCache次之,Encache最差。

参考资料

  1. 缓存那些事
  2. Java本地缓存技术选型
  3. https://github.com/ben-manes/caffeine/wiki/Benchmarks

风是客
19 声望1 粉丝