前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java本地缓存

Java本地缓存

原创
作者头像
一个风轻云淡
修改2024-01-20 18:02:55
3680
修改2024-01-20 18:02:55

前言

缓存是计算机系统中一种常见的数据存储技术。它用于临时存储经常访问的数据,以提高系统的性能和响应速度。

在计算机系统中,数据通常存储在较慢的主存(RAM)中。而缓存则位于主存和处理器之间,作为一个更快、更小的存储器。当处理器需要访问数据时,它首先会查找缓存。如果数据存在于缓存中,就可以快速获取并提供给处理器,这样就避免了读取主存的延迟时间。

缓存利用了局部性原理(Locality Principle),即数据访问模式具有时间局部性和空间局部性。

  • 时间局部性指的是近期访问的数据可能在不久的将来再次被访问
  • 空间局部性指的是与当前访问的数据相邻的数据可能很快被访问。

本地缓存是指将数据暂存到本地计算机的内存中,以便在后续访问中能够更快地获取。本地缓存通常由应用程序使用,可以提高应用程序的性能和响应速度。

Map

在Java中,实现本地缓存通常使用key/value形式的数据结构,可以选择使用Map集合来作为存储容器。常见的Map实现类有HashMap、Hashtable和ConcurrentHashMap。

  • 如果不考虑高并发情况下的数据安全问题,可以选择HashMap。它是非线程安全的,但在单线程或低并发环境下性能较好。
  • 如果需要考虑高并发情况下的数据安全问题,可以选择Hashtable或ConcurrentHashMap。Hashtable是线程安全的,但性能相对较差。而ConcurrentHashMap则既能保证线程安全,又具备较好的性能。

在选择时,更推荐使用ConcurrentHashMap。它通过使用分段锁(Segment)的方式,将数据分成多个段,每个段由独立的锁控制。这样可以提供更好的并发性能,不同的线程可以同时访问不同的段,从而减少了锁竞争的概率。

代码语言:javascript
复制
public class ConcurrentHashMapTest {
    private static ConcurrentHashMap<Integer, Tuple2<Long, String>> cache = new ConcurrentHashMap<>(3);
    private static Map<Integer, String> dbData = new HashMap<>(3);
    static {
        // 初始化数据库数据
        dbData.put(1, "上海");
        dbData.put(2, "北京");
        dbData.put(3, "深圳");
    }
    private static int expirationTime = 3; // 缓存过期时间(单位:秒)
    private static int mill = 1000; // 时间单位换算,从毫秒转为秒
​
    @Test
    @SneakyThrows
    public void test() {
        System.out.println("the result is " + getCityByCode(1)); // 获取城市码为1的城市名
        Thread.sleep(1000); // 等待1秒
        System.out.println("the result is " + getCityByCode(1)); // 再次获取城市码为1的城市名(此时缓存未过期,直接从缓存中获取)
        Thread.sleep(3000); // 等待3秒(超过缓存过期时间)
        System.out.println("the result is " + getCityByCode(1)); // 再次获取城市码为1的城市名(此时缓存已过期,需要重新从数据库获取)
        Thread.sleep(1000); // 等待1秒
        System.out.println("the result is " + getCityByCode(2)); // 获取城市码为2的城市名
    }
​
    private String getCityByCode(int code) {
        if (!cache.containsKey(code)) { // 如果缓存中不包含该城市码的数据
            return getCityFromDb(code); // 从数据库中获取城市名
        }
        Tuple2<Long, String> tuple2 = cache.get(code);
        if (isOverTime(tuple2._1)) { // 如果缓存已超过过期时间
            System.out.println("cache is over time");
            return getCityFromDb(code); // 从数据库中获取城市名
        } else {
            return tuple2._2; // 否则直接从缓存中获取城市名
        }
    }
​
    private String getCityFromDb(Integer code) {
        String city = dbData.get(code); // 从数据库中获取城市名
        System.out.println("query city " + city + " from db");
        cache.put(code, new Tuple2<>(System.currentTimeMillis(), city)); // 将查询结果放入缓存中
        return city;
    }
​
    private boolean isOverTime(Long time) {
        if ((System.currentTimeMillis() - time) / mill > expirationTime) { // 判断是否超过缓存过期时间
            return true; // 已超过过期时间
        }
        return false; // 未超过过期时间
    }
}

GuavaCache

Guava Cache是一个功能强大且易于使用的缓存库,它提供了简单高效的缓存解决方案。

  • Guava Cache(也称为Guava缓存)是Google开源的一个Java库,用于实现本地缓存。它是Guava项目的一部分,是Google对Java集合框架的扩展和增强。
  • Guava Cache提供了一个简单而强大的缓存实现,旨在提高应用程序的性能和响应速度。它支持线程安全,并提供了一些高级特性,例如自动加载缓存、大小限制、过期策略和统计信息收集等。
代码语言:xml
复制
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.0.1-jre</version>
</dependency>
代码语言:javascript
复制
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
​
import java.util.concurrent.TimeUnit;
​
public class GuavaCacheExample {
private static Cache<Integer, String> cache;
public static void main(String[] args) {
    // 创建一个GuavaCache实例
    cache = CacheBuilder.newBuilder()
            .expireAfterWrite(3, TimeUnit.SECONDS) // 设置缓存过期时间为3秒
            .maximumSize(100) // 设置最大缓存大小为100
            .build();
​
    // 添加数据到缓存
    cache.put(1, "shanghai");
    cache.put(2, "beijing");
    cache.put(3, "shenzhen");
​
    // 从缓存中获取数据
    System.out.println("the result is " + getCityByCode(1));
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("the result is " + getCityByCode(1));
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("the result is " + getCityByCode(1));
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("the result is " + getCityByCode(2));
}
​
private static String getCityByCode(int code) {
    String city = cache.getIfPresent(code); // 从缓存中获取城市名
    if (city == null) {
        city = getCityFromDb(code);
        cache.put(code, city); // 将查询结果放入缓存
    }
    return city;
}
​
private static String getCityFromDb(int code) {
    // 模拟从数据库中获取城市名的操作
    switch (code) {
        case 1:
            return "shanghai";
        case 2:
            return "beijing";
        case 3:
            return "shenzhen";
        default:
            return null;
    }
}
}

Caffeine

Caffeine 是基于 JAVA 8 的高性能本地缓存库。并且在 spring5 (springboot 2.x) 后,spring 官方放弃了 Guava,而使用了性能更优秀的 Caffeine 作为默认缓存组件。

Caffeine是在Guava Cache的基础上做一层封装,性能有明显提高,二者同属于内存级本地缓存。使用Caffeine后无需使用Guava Cache,从并发的角度来讲,Caffeine明显优于Guava,原因是使用了Java 8最新的StampedLock锁技术。

本地缓存与分布式缓存对应,缓存进程和应用进程同属于一个JVM,数据的读、写在一个进程内完成。本地缓存没有网络开销,访问速度很快。

Caffeine提供灵活的结构来创建缓存,并且有以下特性:

  • 自动加载条目到缓存中,可选异步方式
  • 可以基于大小剔除
  • 可以设置过期时间,时间可以从上次访问或上次写入开始计算
  • 异步刷新
  • keys自动包装在弱引用中
  • values自动包装在弱引用或软引用中
  • 条目剔除通知
  • 缓存访问统计

简单使用

代码语言:javascript
复制
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.2</version>
</dependency>

入门案例

代码语言:javascript
复制
        // 构建cache对象
        Cache<String, String> cache = Caffeine.newBuilder().build();
​
        // 存数据
        cache.put("k1", "v1");
​
        // 取数据
        String v1 = cache.getIfPresent("k1");
        System.out.println("k1 = " + v1);
​
        // 取数据,包含两个参数:
        // 参数一:缓存的key
        // 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
        // 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
        String defaultkey = cache.get("k2", key -> {
            // 根据key去数据库查询数据
            return "v2";
        });
        System.out.println("k2 = " + defaultkey);

配置案例

代码语言:javascript
复制
public static LoadingCache<Long, User> loadingCache = Caffeine.newBuilder()
    // 初始的缓存空间大小
    .initialCapacity(5)
    // 缓存的最大条数
    .maximumSize(10)
    .expireAfterWrite(4, TimeUnit.SECONDS)
    .expireAfterAccess(10, TimeUnit.SECONDS)
    .refreshAfterWrite(6, TimeUnit.SECONDS)
    .recordStats()
    //设置缓存的移除通知
    .removalListener(new RemovalListener<Long, User>() {
        @Override
        public void onRemoval(@Nullable Long key, @Nullable User user, @NonNull RemovalCause removalCause) {
            System.out.printf("Key: %s ,值:%s was removed!原因 (%s) \n", key, user, removalCause);
        }
    })
    .build(id -> {
        System.out.println("缓存未命中,从数据库加载,用户id:" + id);
        return User.builder().id(id).userName("Lily").age(new Random().nextInt(20)).build();
    });

参数说明:

  • initialCapacity 初始的缓存空间大小
  • maximumSize 缓存的最大条数
  • maximumWeight 缓存的最大权重
  • expireAfterAccess 最后一次写入或访问后,经过固定时间过期
  • expireAfterWrite 最后一次写入后,经过固定时间过期
  • refreshAfterWrite 写入后,经过固定时间过期,下次访问返回旧值并触发刷新
  • weakKeys 打开 key 的弱引用
  • weakValues 打开 value 的弱引用
  • softValues 打开 value 的软引用
  • recordStats 缓存使用统计
  • expireAfterWrite 和 expireAfterAccess 同时存在时,以 expireAfterWrite 为准。
  • weakValues 和 softValues 不可以同时使用。
  • maximumSize 和 maximumWeight 不可以同时使用。

清除策略

Caffeine提供了三种缓存驱逐策略:

基于容量:设置缓存的数量上限

代码语言:javascript
复制
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(1) // 设置缓存大小上限为 1
    .build();

基于时间:设置缓存的有效时间

代码语言:javascript
复制
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
    // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
    .expireAfterWrite(Duration.ofSeconds(10)) 
    .build();

基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

代码语言:javascript
复制
    // 构建cache对象
        Cache<String, String> cache = Caffeine.newBuilder()
                .weakKeys().weakValues().build();

Caffeine.weakKeys() 使用弱引用存储key。如果没有强引用这个key,则GC时允许回收该条目 Caffeine.weakValues() 使用弱引用存储value。如果没有强引用这个value,则GC时允许回收该条目 Caffeine.softValues() 使用软引用存储value, 如果没有强引用这个value,则GC内存不足时允许回收该条目

引用类型

被垃圾回收时间

用途

生存时间

强引用

从来不会

对象的一般状态

JVM停止运行时终止

软引用

在内存不足时

对象缓存

内存不足时终止

弱引用

在垃圾回收时

对象缓存

gc运行后终止

虚引用

Unknown

Unknown

Unknown

GuavaCache和Caffeine差异

  • 剔除算法方面,GuavaCache采用的是「LRU」算法,而Caffeine采用的是「Window TinyLFU」算法,这是两者之间最大,也是根本的区别。
  • 立即失效方面,Guava会把立即失效 (例如:expireAfterAccess(0) and expireAfterWrite(0)) 转成设置最大Size为0。这就会导致剔除提醒的原因是SIZE而不是EXPIRED。Caffiene能正确识别这种剔除原因。
  • 取代提醒方面,Guava只要数据被替换,不管什么原因,都会触发剔除监听器。而Caffiene在取代值和先前值的引用完全一样时不会触发监听器。
  • 异步化方方面,Caffiene的很多工作都是交给线程池去做的(默认:ForkJoinPool.commonPool()),例如:剔除监听器,刷新机制,维护工作等。

EhCache

EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认CacheProvider。Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。

缓存支持三种类型:堆内存储、堆外存储、磁盘存储(支持持久化)。

使用方法如下:

代码语言:javascript
复制
<dependency><dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.9.7</version>
</dependency>
代码语言:javascript
复制
@Slf4j
public class EhcacheTest {
    private static final String ORDER_CACHE = "orderCache";
    public static void main(String[] args) {
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
                // 创建实例
                .withCache(ORDER_CACHE, CacheConfigurationBuilder
              // 声明一个容量为30的堆内缓存
                        .newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(30)))
                .build(true);
        // 得到缓存实例
        Cache<String, String> cache = cacheManager.getCache(ORDER_CACHE, String.class, String.class);
​
        String orderId = String.valueOf(6666666);
        String orderInfo = cache.get(orderId);
        if (StrUtil.isBlank(orderInfo)) {
            orderInfo = getInfo(orderId);
            cache.put(orderId, orderInfo);
        }
        log.info("orderInfo = {}", orderInfo);
    }
​
    private static String getInfo(String orderId) {
        String info = "";
        // 首先从redis查
        log.info("get data from redis");
        // 不存在 查db
        log.info("get data from mysql");
        info = String.format("{orderId=%s}", orderId);
        return info;
    }
}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Map
  • GuavaCache
  • Caffeine
    • 简单使用
      • 配置案例
        • 清除策略
          • GuavaCache和Caffeine差异
          • EhCache
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档