本地缓存组件

Ehcache

Ehcache is an open source, standards-based cache that boosts performance, offloads your database, and simplifies scalability. It’s the most widely-used Java-based cache because it’s robust, proven, full-featured, and integrates with other popular libraries and frameworks. Ehcache scales from in-process caching, all the way to mixed in-process/out-of-process deployments with terabyte-sized caches.

引入ehcache依赖(当前最新版本3.10.1)

参考官方文档:Ehcache 3.10 Documentation

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.1</version>
</dependency>

基于Ehcache 3 API示例

    @Test
    @DisplayName("Ehcache 3 API")
    public void cache_test() {
        // 构建CacheManager同时声明preConfigured缓存
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                .withCache("preConfigured",
                        CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
                                        ResourcePoolsBuilder.heap(100))
                                .build())
                .build(true);

        // 获取preConfigured缓存
        Cache<Long, String> preConfigured
                = cacheManager.getCache("preConfigured", Long.class, String.class);

        // 通过cacheManager创建缓存
        Cache<Long, String> myCache = cacheManager.createCache("myCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
                        ResourcePoolsBuilder.heap(100)).build());

        myCache.put(1L, "one!");
        String value = myCache.get(1L);
        Assertions.assertEquals("one!", value);
        cacheManager.close();
    }

详细用法参考:Ehcache 3.10 Documentation

Guava Cache

LoadingCache的几个获取缓存方法:

  • V get(K key) throws ExecutionException;

使用这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。在加载完成之前,不会修改与此缓存关联的可观察状态。 如果另一个get或getUnchecked调用当前正在加载key的值,只需等待该线程完成并返回其加载的值。请注意,多个线程可以同时加载不同键的值。声明为抛出ExecutionException的异常。

  • V getUnchecked(K key);

使用这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。在加载完成之前,不会修改与此缓存关联的可观察状态。与get不同,此方法不抛出已检查异常,因此只应在缓存加载程序未抛出已检测异常的情况下使用。如果你定义的CacheLoader没有声明任何检查型异常,则可以通过 getUnchecked(K) 查找缓存;但必须注意,一旦CacheLoader声明了检查型异常,就不可以调用getUnchecked(K)。请注意,多个线程可以同时加载不同键的值。

  • V getIfPresent(@CompatibleWith(“K”) Object key);(@CheckForNull )

返回与此缓存中的键关联的值,如果没有键的缓存值,则返回null。

  • ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException;

返回与键关联的值的映射,必要时创建或检索这些值。返回的映射包含已缓存的条目,以及新加载的条目;它永远不会包含空键或值。方法用来执行批量查询。默认情况下,对每个不在缓存中的键,getAll方法会单独调用CacheLoader.load来加载缓存项。可以通过重写load()方法来提高加载缓存的效率;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalNotification;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * 单独使用guava的cache,guava的cache分为两种
 * 第一种:com.google.common.cache.LocalCache.LocalLoadingCache
 * 缓存中获取不到值,会根据指定的Loader进行加载,加载后自动放入缓存
 * 第二种:com.google.common.cache.LocalCache.LocalManualCache
 * 类似ehcache
 */
@Slf4j
public class GuavaCacheTest {

    @DisplayName("LoadingCache")
    @Test
    public void cache_test() {
        LoadingCache<Long, String> loadingCache = CacheBuilder.newBuilder()
                // 指定并发级别
                .concurrencyLevel(8)
                // 初始化大小,配合concurrencyLevel做分段锁
                .initialCapacity(60)
                // 缓存中最多能放置多少个元素
                .maximumSize(10)
                // 从写入开始计算,10秒钟后过期
                .expireAfterWrite(10L, TimeUnit.SECONDS)
                // 统计命中率
                .recordStats()
                // 缓存中的元素被驱逐出去后,会自动回调,但是过期不会自动触发
                .removalListener((RemovalNotification<Long, String> notification) -> {
                    Long key = notification.getKey();
                    RemovalCause cause = notification.getCause();
                    log.info("Key : {} remove because : {}", key, cause);
                })
                // 缓存中获取不到值,会根据指定的Loader进行加载,加载后自动放入缓存
                .build(new CacheLoader<Long, String>() {
                    // key: 将来使用loadingCache.get(key)获取不到传来的key
                    @Override
                    public String load(Long key) throws Exception {
                        // 可以在这里进行数据的加载
                        log.info("去存储中加载");
                        return new StringBuilder("存储中加载出的结果 --> 对应key为:").append(key).toString();
                    }
                });
        // 不会加载load方法,打印结果为null
        System.out.println(loadingCache.getIfPresent(10L));
        String value = "";
        try {
            // get方法抛异常,需要捕获
            value = loadingCache.get(10L);
        } catch (ExecutionException exception) {
            log.error(exception.getMessage(), exception);
        }
        log.info("结果为:{}", value);
        try {
            TimeUnit.SECONDS.sleep(8L);
        } catch (InterruptedException exception) {
            log.error(exception.getMessage(), exception);
        }
        // 未过期,直接从缓存中加载
        log.info("结果为:{}", loadingCache.getUnchecked(10L));
        try {
            TimeUnit.SECONDS.sleep(5L);
        } catch (InterruptedException exception) {
            log.error(exception.getMessage(), exception);
        }
        // 重新加载
        log.info("结果为:{}", loadingCache.getUnchecked(10L));
        // 生产环境禁止使用统计信息
        log.info("统计信息为:{}", loadingCache.stats().toString());
    }
}

打印结果如下:

null
23:35:22.212 [main] INFO com.lwy.it.GuavaCacheTest - 去存储中加载
23:35:22.216 [main] INFO com.lwy.it.GuavaCacheTest - 结果为:存储中加载出的结果 --> 对应key为:10
23:35:30.222 [main] INFO com.lwy.it.GuavaCacheTest - 结果为:存储中加载出的结果 --> 对应key为:10
23:35:35.230 [main] INFO com.lwy.it.GuavaCacheTest - Key : 10 remove because : EXPIRED
23:35:35.230 [main] INFO com.lwy.it.GuavaCacheTest - 去存储中加载
23:35:35.230 [main] INFO com.lwy.it.GuavaCacheTest - 结果为:存储中加载出的结果 --> 对应key为:10
23:35:35.232 [main] INFO com.lwy.it.GuavaCacheTest - 统计信息为:CacheStats{hitCount=1, missCount=3, loadSuccessCount=2, loadExceptionCount=0, totalLoadTime=4183416, evictionCount=1}

可以看到removalListener过期后并没有自动触发?

GuavaCache 并不保证在过期时间到了之后立刻删除该 Key,如果你此时去访问了这个 Key,它会检测是不是已经过期,过期就删除它,所以过期时间到了之后你去访问这个 Key 会显示这个 Key 已经被删除,但是如果你不做任何操作,那么在 时间 到了之后也许这个 Key 还在内存中。GuavaCache 选择这样做的原因也很简单,如下:

The reason for this is as follows: if we wanted to perform Cache maintenance continuously, we would need to create a thread, and its operations would be competing with user operations for shared locks. Additionally, some environments restrict the creation of threads, which would make CacheBuilder unusable in that environment.

这样做既可以保证对 Key 读写的正确性,也可以节省资源,减少竞争。

常用方法示例:

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

@Slf4j
public class GuavaCacheDemo {
    private static final LoadingCache<String, Integer> CACHE = CacheBuilder.newBuilder()
            // 设置大小和条目数
            .maximumSize(20)
            /**
             * 指定在条目创建、最近一次替换其值或上次访问后经过固定持续时间后,应自动从缓存中删除每个条目。
             * 访问时间由所有缓存读取和写入操作(包括Cache.asMap().get(Object)和Cache.asMap().put(K,V) )重置,
             * 但不是由containsKey(Object)或对操作Cache.asMap的集合视图。因此,例如,遍历Cache.asMap().entrySet() 不会重置您检索的条目的访问时间。
             */
            .expireAfterAccess(20, TimeUnit.SECONDS)
            // 清除缓存监听器
            .removalListener((notification) -> {
                log.info("{}移出原因:{}", notification.getKey(), notification.getCause());
            })
            .build(new CacheLoader<String, Integer>() {
                @Override
                public Integer load(String key) throws Exception {
                    log.info("当缓存中不存在时加载");
                    return -1;
                }
            });

    /**
     * 通过key获取缓存的value
     * 如果key不存在,将调用CacheLoader#load()方法再加载其它的数据
     *
     * @param key 缓存key
     * @return value
     */
    public static Integer get(String key) {
        try {
            return CACHE.get(key);
        } catch (ExecutionException exception) {
            log.error(exception.getMessage(), exception);
        }
        return null;
    }

    /**
     * 移出缓存
     *
     * @param key 缓存key
     */
    public static void remove(String key) {
        CACHE.invalidate(key);
    }

    /**
     * 全部清空缓存
     */
    public static void removeAll() {
        CACHE.invalidateAll();
    }

    /**
     * 保存缓存数据
     * 如果缓存中已经有key,则会先移出这个缓存,这个时候会触发removalListener监听器,触发之后再添加这个key和value
     *
     * @param key   缓存key
     * @param value 缓存值value
     */
    public static void put(String key, Integer value) {
        CACHE.put(key, value);
    }

    /**
     * 查询缓存中所有数据的Map
     *
     * @return 缓存
     */
    public static ConcurrentMap<String, Integer> viewCaches() {
        return CACHE.asMap();
    }
}
Caffeine Cache

Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。

缓存和ConcurrentMap有点相似,但还是有所区别。最根本的区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。但是,Caffeine的缓存Cache 通常会被配置成自动驱逐缓存中元素,以限制其内存占用。在某些场景下,LoadingCacheAsyncLoadingCache 因为其自动加载缓存的能力将会变得非常实用。

Caffeine提供了灵活的构造器去创建一个拥有下列特性的缓存:

为了提高集成度,扩展模块提供了JSR-107 JCacheGuava适配器。JSR-107规范了基于Java 6的API,在牺牲了功能和性能的代价下使代码更加规范。Guava的Cache是Caffeine的原型库并且Caffeine提供了适配器以供简单的迁移策略。

中文文档:https://github.com/ben-manes/caffeine/wiki/Home-zh-CN

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

注意:2.9.3仍支持jdk8 编译,caffeine 3.0.0 以上不支持 JDK8 编译

The release notes for 3.0 state,

Highlights
  • Java 11 or above is required

  • Java 8 users can continue to use version 2.x, which will be supported

We still support v2 for Java 8 users, and released 2.9.1 after the 3.0 release to include relevant updates. The only limitation going forward is that that there is no plan to backport any future feature additions. That shouldn’t be an issue since this library is already pretty much feature complete.

The JDK change was required to replace usages of sun.misc.Unsafe with VarHandles, which was released in Java 9. We waited until enough changes warranting a major version bump to do this, so by semver it more clearly allows for backwards incompatible changes.

I hope that eases your concerns. Please continue to use v2 and trust that it will be supported. The releases are performed by github actions so there won’t be a mistake of running it locally with the wrong JDK.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值