Java本地缓存

2024-01-20 18:02:55 浏览数 (1)

前言

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

在计算机系统中,数据通常存储在较慢的主存(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;
    }
}

0 人点赞