当前位置: 首页 > news >正文

本地缓存组件

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提供了灵活的构造器去创建一个拥有下列特性的缓存:

  • 自动加载元素到缓存当中,异步加载的方式也可供选择
  • 当达到最大容量的时候可以使用基于就近度和频率的算法进行基于容量的驱逐
  • 将根据缓存中的元素上一次访问或者被修改的时间进行基于过期时间的驱逐
  • 当向缓存中一个已经过时的元素进行访问的时候将会进行异步刷新
  • key将自动被弱引用所封装
  • value将自动被弱引用或者软引用所封装
  • 驱逐(或移除)缓存中的元素时将会进行通知
  • 写入传播到一个外部数据源当中
  • 持续计算缓存的访问统计指标

为了提高集成度,扩展模块提供了JSR-107 JCache和Guava适配器。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.

相关文章:

  • 掌握这25个单行代码技巧,你也能写出『高端』Python代码
  • 看过来,这里教你如何更好地呈现荣誉证书
  • 计算机视觉图像公开数据集最全汇总
  • 【C++】C++ 引用
  • 量子兔 alphapi 硬件拆解分析 尝试自己从头写库
  • linux常用命令(Beginner note)
  • IDEA+SpringCloudAlibaba微服务搭建
  • 【Python刷题篇】——Python入门 011面向对象(二)
  • 使用Quartz或CronUtil实现动态的任务调度以及任务的管理
  • 死磕它3年,入职京东,要个25K不过分吧?
  • 乐高CPC认证办理儿童玩具出口美国亚马逊CPSIC认证
  • .NET Core Web APi类库如何内嵌运行?
  • Kafka3.2.3基于Linux的集群安装(待续)
  • 数据湖技术之 Hudi 框架概述
  • 前端利器 —— 提升《500倍开发效率》 传一张设计稿,点击一建生成项目 好牛
  • 3.7、@ResponseBody 和 @RestController
  • Android 架构优化~MVP 架构改造
  • js算法-归并排序(merge_sort)
  • Linux后台研发超实用命令总结
  • node入门
  • puppeteer stop redirect 的正确姿势及 net::ERR_FAILED 的解决
  • python docx文档转html页面
  • spring security oauth2 password授权模式
  • Windows Containers 大冒险: 容器网络
  • 百度小程序遇到的问题
  • 基于webpack 的 vue 多页架构
  • 前端_面试
  • 使用 Docker 部署 Spring Boot项目
  • 学习笔记TF060:图像语音结合,看图说话
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • $(selector).each()和$.each()的区别
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (51单片机)第五章-A/D和D/A工作原理-A/D
  • (C语言)strcpy与strcpy详解,与模拟实现
  • (libusb) usb口自动刷新
  • (多级缓存)多级缓存
  • (二) Windows 下 Sublime Text 3 安装离线插件 Anaconda
  • (附源码)springboot助农电商系统 毕业设计 081919
  • (论文阅读笔记)Network planning with deep reinforcement learning
  • (免费分享)基于springboot,vue疗养中心管理系统
  • (四)TensorRT | 基于 GPU 端的 Python 推理
  • (学习日记)2024.02.29:UCOSIII第二节
  • (原創) 如何優化ThinkPad X61開機速度? (NB) (ThinkPad) (X61) (OS) (Windows)
  • (转) 深度模型优化性能 调参
  • (转载)虚函数剖析
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .net core webapi 部署iis_一键部署VS插件:让.NET开发者更幸福
  • .Net MVC4 上传大文件,并保存表单
  • .NET 中 GetHashCode 的哈希值有多大概率会相同(哈希碰撞)
  • .NET/C# 使用 ConditionalWeakTable 附加字段(CLR 版本的附加属性,也可用用来当作弱引用字典 WeakDictionary)
  • .NetCore项目nginx发布
  • .xml 下拉列表_RecyclerView嵌套recyclerview实现二级下拉列表,包含自定义IOS对话框...
  • //解决validator验证插件多个name相同只验证第一的问题
  • /boot 内存空间不够
  • @require_PUTNameError: name ‘require_PUT‘ is not defined 解决方法