【一篇文章理解Java中多级缓存的设计与实现】

news2024/12/26 18:00:22

文章目录

    • 一.什么是多级缓存?
      • 1.本地缓存
      • 2.远程缓存
      • 3.缓存层级
      • 4.加载策略
    • 二.适合/不适合的业务场景
      • 1.适合的业务场景
      • 2.不适合的业务场景
    • 三.Redis与Caffine的对比
      • 1. 序列化
      • 2. 进程关系
    • 四.各本地缓存性能测试对比报告(官方)
    • 五.本地缓存Caffine如何使用
      • 1. 引入maven依赖:
      • 2.关于Caffine的各api操作介绍
    • 六.多级缓存方案与实现思路
    • 七.小结

一.什么是多级缓存?

多级缓存技术是一种通过多个层次的缓存来提高数据访问速度和降低延迟的策略。多级缓存通过在不同层次上缓存数据来减少对底层存储系统的访问次数,提高系统的整体性能。在Java中,常见的多级缓存结构包括:本地缓存与远程缓存。

1.本地缓存

Caffeine/Guava/jdk下的线程安全Map等等,因为Caffine性能最高,我这里本地缓存都代指Caffine。在应用程序的内存中存储数据,访问速度极快,但容量有限。

Caffeine是一个基于Java 8的高性能缓存库,它提供了高性能、高命中率、低内存占用的特性,被誉为最快的缓存之一
Caffeine是一个基于Java 8的高性能缓存库,它提供了高性能、高命中率、低内存占用的特性,被誉为最快的缓存之一。
JDK内置的Map可作为缓存的一种实现方式,然而严格意义来讲,其不能算作缓存的范畴。原因如下:一是其存储的数据不能主动过期;二是无任何缓存淘汰策略。

2.远程缓存

如Redis/Memcached:在网络中存储数据,容量大,但访问速度相对较慢。因为我没用过Memcached,这里远程缓存代指Redis。

3.缓存层级

  • 一级缓存(本地缓存):直接与应用程序关联,适合频繁访问的数据。
  • 二级缓存(远程缓存):作为一级缓存的补充,存储相对较不常访问的数据。

4.加载策略

  • 先从本地缓存获取数据,如果不存在,再去远程缓存获取,最后如仍不存在,则从数据库获取并缓存到远程和本地。

这种多级缓存结构能有效提高应用程序性能,降低数据库压力

二.适合/不适合的业务场景

1.适合的业务场景

Caffeine 适合需要快速访问、短期存储的数据场景,如频繁查询的热点数据、计算结果缓存等,尤其是在高并发环境下表现优异。
它特别适用于以下业务场景:

  • 常用数据的枚举值‌:例如类目数据,这类数据变更频率低,且对实时性要求不高,适合使用Caffeine进行缓存。‌
  • 依赖第三方系统的一些不频繁变更的键值对‌:先在本地缓存中查找,如果存在则直接返回,不存在则调用第三方系统获取数据并存入本地缓存中。这种模式适用于那些不是经常变化的数据,可以减少对外部系统的依赖,提高系统响应速度。

2.不适合的业务场景

Caffeine不适合实时性要求高或数据变更频繁的场景,对于需要持久存储的数据,或是数据更新频繁且需要实时一致性的场景,就不太适合,因为 Caffeine 的数据是保存在内存中的,可能会导致数据丢失或不一致。因为这些场景对数据的实时性和准确性要求极高,而Caffeine的设计初衷是为了提供高性能的本地缓存,而不是实时同步外部数据源的变化。此外,Caffeine也不适合需要强一致性保证的数据存储,因为它主要关注性能和命中率,而不是数据的一致性。

总的来说,Caffeine适合那些对数据变更频率不高、对实时性要求不是特别严格的应用场景,通过减少对外部数据源的访问次数,提高系统的整体性能和响应速度‌。

个人认为:其实不单单是我们本地缓存,就是分布式缓存Redis也不适合数据变更频繁的业务场景。引入缓存的本质是为了提高性能减少db操作,但是面对db修改频繁的场景又是引入本地缓存又是分布式缓存,又用其他中间件去解决这个不一致性(更何况哪天你们公司真正高并发起来这个不一致性还无法完全解决,这就是系统的一个坑埋在这了),所以个人觉得db修改频繁就不应该使用缓存!!!
网上人家经常说什么高并发下如何保证缓存与数据库一致性: 比如1.通过延时双删。2.使用canal(增量日志并提供增量数据的订阅与消费)获取到变更数据则更新缓存, 3.使用消息队列等等一系列措施。个人觉得这本来就是个伪命题,高并发下你对数据变更频繁的场景使用缓存真的就合适吗?真正高并发下用了这些,但凡一丁点中间件的网络波动一致性也是无法完全保证的,高并发下缓存与数据库一致性就是个无法完全解不了的问题,只能减少不一致。 当然如果并发量少使用上述的方案基本不会有问题,但是想想我们这个使用缓存+ 中间件的成本真的就比查询一次db低吗。

三.Redis与Caffine的对比

从横向对常用的缓存进行对比,有助于加深对缓存的理解,有助于提高技术选型的合理性。下面对比缓存:Redis、Caffeine。

1. 序列化

  • Redis必须实现序列化。进程间数据传输,因此必须实现序列化。大多数情况下涉及内网网络传输;作为缓存数据库使用,持久化是标配。
  • Caffeine不需要实现序列化。Map对象的改进型接口,不涉及任何形式的网络传输和持久化,因此完全不需要实现序列化接口。

2. 进程关系

  • Redis与业务进程独立,业务系统重启对缓存服务无影响,Redis服务与业务服务独立,互相影响较小
  • Caffeine附着于业务进程,业务系统重启缓存数据会全部丢失,纯内存型缓存与业务系统属于同一个JVM

四.各本地缓存性能测试对比报告(官方)

以下是Caffeine官方给出的基准测试结果,在与其他的本地缓存性能对比中身居第一位!。Caffeine的读写性能要远好于Guava,甚至超过不带缓存特性的ConcurrentHashMap。
具体详见官方给出的基本测试报告:https://github.com/ben-manes/caffeine/wiki/Benchmarks-zh-CN

生成计算
在这个 基准测试 中,缓存是无界且被完全填充的,并且生成计算的结果将返回一个常量。这个基准测试体现了生成计算元素的时候将当前元素加锁产生的开销。如果调用不存在,Caffeine 首先会进行一次无锁的预筛选,在进行原子操作。绘图的场景是所有线程对(“sameKey”)进行查询,并基于Zipf在各个线程中查询不同的key(“spread”)。
在这里插入图片描述
读 (100%)
在这个基准测试中, 8 线程对一个配置了最大容量的缓存进行并发读。
在这里插入图片描述

读 (75%) / 写 (25%)
在这个基准测试 中,对一个配置了最大容量的缓存,6 线程 进行并发读,2 线程进行并发写。
在这里插入图片描述

写 (100%)
在这个基准测试 中,8 线程对一个配置了最大容量的缓存进行并发写。
在这里插入图片描述

五.本地缓存Caffine如何使用

通过官方的基准测试,所以我们既然要用到本地缓存机制(例如需要用到缓存过期、过期监听、淘汰策略)等,选型那就用性能最厉害的Caffine,它支持多种缓存策略,如基于大小、时间的过期策略等。下面是一些常用的操作 API 及其示例代码。

1. 引入maven依赖:

   <!-- 本地缓存 -->
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.9.3</version>
        </dependency>

2.关于Caffine的各api操作介绍

1. 创建缓存

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

Cache<String, String> cache = Caffeine.newBuilder()
	// 设置最大缓存条目数
    .maximumSize(100) 
    // 设置写入后的过期时间
    .expireAfterWrite(10, TimeUnit.MINUTES) 
    // 初始的缓存空间大小
    .initialCapacity(20)
    // 缓存的最大条数
    .maximumSize(100)
    .removalListener(((key,value,cause)->{
        log.info("缓存失效通知,key:{},原因:{}",key,cause);
    }))
    .build();
2. 基本的缓存操作,添加、查询、删除缓存值

// 存入缓存
cache.put("key1", "value1");

// 获取缓存值
String value = cache.getIfPresent("key1");
System.out.println(value); // 输出: value1

// 缓存中有key2则返回缓存中的值,缓存中没有key2的值,则通过loadFromDatabase方法从数据库或其他来源加载数据并存入缓存。
String value = cache.get("key2", key -> loadFromDatabase(key));
// 输出从数据库加载的值
System.out.println(value); 

// 删除某个缓存项
cache.invalidate("key1"); 

// 清空所有缓存项
cache.invalidateAll(); 

3. 异步加载缓存
Caffeine 也支持异步加载缓存,当缓存项不存在时,异步调用加载方法。

AsyncCache<String, String> asyncCache = Caffeine.newBuilder()
    .maximumSize(100)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .buildAsync();

// 异步获取缓存值
CompletableFuture<String> futureValue = asyncCache.get("key3", key -> loadFromDatabaseAsync(key));

// 异步处理获取结果
futureValue.thenAccept(value -> System.out.println("Value: " + value));

4. 基于时间的过期策略
Caffeine 支持基于时间的缓存过期机制,如写入后的过期、访问后的过期等。

写入后过期
Cache<String, String> cache = Caffeine.newBuilder()
    .expireAfterWrite(5, TimeUnit.MINUTES) // 写入后 5 分钟过期
    .build();
    
访问后过期
Cache<String, String> cache = Caffeine.newBuilder()
    .expireAfterAccess(5, TimeUnit.MINUTES) // 访问后 5 分钟过期
    .build();
5. 基于缓存大小的淘汰策略
你可以通过 maximumSize 或 maximumWeight 方法设置缓存的大小限制。

按照缓存项的数量限制
Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(100) // 最多存储 100 条记录
    .build();
    
按照缓存项的权重限制
Cache<String, String> cache = Caffeine.newBuilder()
    .maximumWeight(1000) // 总权重限制为 1000
    .weigher((key, value) -> value.length()) // 以值的长度为权重
    .build();
    
6. 基于软引用或弱引用的缓存
Caffeine 支持使用软引用或弱引用存储缓存值,当 JVM 内存不足时可以自动回收这些缓存。

使用弱引用存储键
Cache<String, String> cache = Caffeine.newBuilder()
    .weakKeys() // 使用弱引用存储键
    .build();
    
使用软引用存储值
Cache<String, String> cache = Caffeine.newBuilder()
    .softValues() // 使用软引用存储值
    .build();
    
7. 统计缓存命中率
Caffeine 支持记录缓存的命中率、加载时间等统计信息。
Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(100)
    .recordStats() // 启用统计信息
    .build();

// 获取统计信息
System.out.println(cache.stats());

8.LoadingCache 的结合
LoadingCacheCaffeine 提供的一个更高级的缓存操作类,它支持自动同步加载数据的功能。

LoadingCache<String, String> loadingCache = Caffeine.newBuilder()
    .maximumSize(100)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> loadFromDatabase(key)); // 自动加载缓存

// 直接获取缓存值,如果缓存中没有则调用 `loadFromDatabase`
String value = loadingCache.get("key1");
System.out.println(value);

总结:Caffeine 提供了丰富的 API 来满足不同业务场景的缓存需求。它不仅支持基本的缓存操作,还提供了多种淘汰策略、异步缓存以及统计功能,适用于多种场景。

从上面8点中,有没人发现第8点:loadingCache.get(“key1”)与第2点 cache.get(“key2”, key -> loadFromDatabase(key)); 功能基本一致?都是实现缓存中有则从缓存中取,缓存中没有则从db查询并存入缓存中。 只不过是加载逻辑的定义不同,一个是在 build() 时预定义,一个是每次 get() 时传递加载逻辑。

  • 何时选择 LoadingCache ?
    如果所有的键加载逻辑相同,你可以事先定义加载方式,并希望缓存缺失时自动加载数据,LoadingCache 是理想的选择。它提供了简洁的接口和良好的同步处理。
  • 何时选择 Cache.get(key, keyMapper) ?
    如果每个键的加载逻辑不同,或你希望在每次获取时灵活指定加载方式,那么 Cache.get(key, keyMapper) 更加合适。它提供了更大的灵活性来动态处理不同的缓存加载需求。
  • 两种方案实现缓存中有?从缓存中取 :db查询再塞入缓存,总结:
    LoadingCache 适用于需要统一加载策略、且不需要每次都指定加载逻辑的场景。Cache.get(key, keyMapper) 适用于需要根据具体情况动态指定加载逻辑的场景,更加灵活但相对复杂。你可以根据自己的业务场景选择合适的缓存操作方式。

六.多级缓存方案与实现思路

下面是一个简单的多级缓存实现示例,结合了Caffeine作为本地缓存和Redis作为远程缓存。我们在项目里面可以把缓存定义成配置bean, redis可以使用RedisTemplate。 这样一个多级缓存机制就实现啦,是不是很简单。

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import redis.clients.jedis.Jedis;

import java.util.concurrent.TimeUnit;

public class MultiLevelCache {
    private final Cache<String, String> localCache;
    private final Jedis remoteCache;
    public MultiLevelCache() {
    	// 创建本地缓存Caffine
        localCache = Caffeine.newBuilder()
							 // 设置最大缓存条目数
						     .maximumSize(100) 
						     // 设置写入后的过期时间
						     .expireAfterWrite(10, TimeUnit.MINUTES) 
						     // 初始的缓存空间大小
						     .initialCapacity(20)
						     // 缓存的最大条数
						     .maximumSize(100)
						     .removalListener(((key,value,cause)->{
						         log.info("缓存失效通知,key:{},原因:{}",key,cause);
						     }))
						     .build();
		// Redis连接
        remoteCache = new Jedis("localhost"); 
    }

    public String getData(String key) {
        // 先从本地缓存获取
        String value = localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }

        // 本地缓存未命中,尝试从远程缓存获取
        value = remoteCache.get(key);
        if (value != null) {
            // 更新本地缓存
            localCache.put(key, value); 
            return value;
        }

        // 最后从数据库获取(假设为getDataFromDatabase方法)
        value = getDataFromDatabase(key);
        
        // 更新远程和本地缓存
        remoteCache.set(key, value);
        localCache.put(key, value);
        return value;
    }

    private String getDataFromDatabase(String key) {
        // 模拟数据库查询
        return "DatabaseValueFor:" + key;
    }
}

七.小结

  • 主要介绍了什么是多级缓存:什么是本地缓存、什么是分布式缓存,本地缓存比分布式缓存快的原因。各本地缓存的性能对比中Caffine的性能是最高的,Caffine的Api使用,多级缓存的设计与实现等等。
  • 谈到接口性能优化,我们除了sql调优还能从哪些方面优化?ok,当然是多级缓存技术方案啦!合适的业务场景下使用redis配合本地缓存,效率又能提升些。
  • 除了缓存技术呢? ok,比如使用数据传输上的压缩,像请求参数,或者使用OpenFeign进行rpc调用响应值等等这些都可以使用GZIP压缩数据传输。 像OpenFeign底层http连接是通过jdk下的URLConnection,我们可以引入Apach 下的HttpClient, 或者okhttp 等,这些底层有用到连接池,可以复用连接等等。这些全都是我们接口性能的一些优化手段。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2184596.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

陶瓷4D打印有挑战,水凝胶助力新突破,复杂结构轻松造

大家好&#xff01;今天要和大家聊聊一项超酷的技术突破——《Direct 4D printing of ceramics driven by hydrogel dehydration》发表于《Nature Communications》。我们都知道4D打印很神奇&#xff0c;能让物体随环境变化而改变形状。但陶瓷因为太脆太硬&#xff0c;4D打印一…

java中创建不可变集合

一.应用场景 二.创建不可变集合的书写格式&#xff08;List&#xff0c;Set&#xff0c;Map) List集合 package com.njau.d9_immutable;import java.util.Iterator; import java.util.List;/*** 创建不可变集合:List.of()方法* "张三","李四","王五…

鸿蒙开发选择表情

鸿蒙开发选择表情 动态评论和聊天信息都需要用到表情&#xff0c;鸿蒙是没有提供的&#xff0c;得自己做 一、思路&#xff1a; 用表情字符显示表情&#xff0c;类似0x1F600代表笑脸 二、效果图&#xff1a; 三、关键代码&#xff1a; // 联系&#xff1a;893151960 Colum…

蓝桥杯【物联网】零基础到国奖之路:十五. 扩展模块之双路ADC

蓝桥杯【物联网】零基础到国奖之路:十五. 扩展模块之双路ADC 第一节 硬件解读第二节 CubeMX配置第三节 代码编写 第一节 硬件解读 STM32的ADC是12位&#xff0c;通过硬件过采样扩展到16位&#xff0c;模数转换器嵌入到STM32L071xx器件中。有16个外部通道和2个内部通道&#xf…

PDF阅读器工具集萃:满足你的多样需求

现在阅读书籍大部分都喜欢电子书的形式了吧&#xff0c;因为小小的一个设备就能存下上万本书。从流传程度来说PDF无疑是一个使用最广的格式。除了福昕PDF阅读器阅读之外还有哪些好用的阅读工具呢/&#xff1f;今天我们一起来探讨一下吧。 1.福昕阅读器 链接一下>>www.f…

css3-----2D转换、动画

2D 转换&#xff08;transform&#xff09; 转换&#xff08;transform&#xff09;是CSS3中具有颠覆性的特征之一&#xff0c;可以实现元素的位移、旋转、缩放等效果 移动&#xff1a;translate旋转&#xff1a;rotate缩放&#xff1a;scale 二维坐标系 2D 转换之移动 trans…

SysML案例-清朝、火星人入侵地球

DDD领域驱动设计批评文集>> 《软件方法》强化自测题集>> 《软件方法》各章合集>> 以下图形摘自Jon Holt和Simon Perry的SysML for Systems Engineering。 案例素材来自H. G. Wells在1898年&#xff08;没错&#xff0c;清朝&#xff09;出版的The War of…

Netty系列-7 Netty编解码器

背景 netty框架中&#xff0c;自定义解码器的起点是ByteBuf类型的消息, 自定义编码器的终点是ByteBuf类型。 1.解码器 业务解码器的起点是ByteBuf类型 netty中可以通过继承MessageToMessageEncoder类自定义解码器类。MessageToMessageEncoder继承自ChannelInboundHandlerAdap…

用于高频交易预测的最优输出LSTM

用于高频交易预测的最优输出LSTM J.P.Morgan的python教程 Content 本文提出了一种改进的长短期记忆&#xff08;LSTM&#xff09;单元&#xff0c;称为最优输出LSTM&#xff08;OPTM-LSTM&#xff09;&#xff0c;用于实时选择最佳门或状态作为最终输出。这种单元采用浅层拓…

CSS 盒子属性

1. 盒子模型组成 1.1 边框属性 1.1.1 四边分开写 1.1.2 合并线框 1.1.3 边框影响盒子大小 1.2 内边距 注意&#xff1a; 1.3 外边距 1.3.1 嵌套块元素垂直外边距的塌陷 1.4 清除内外边距 1.5 总结

使用YOLO11训练自己的数据集【下载模型】-【导入数据集】-【训练模型】-【评估模型】-【导出模型】

目录 前言&#xff1a;一、下载模型二、导入数据集三、训练自己的数据集四、验证数据集五、测试数据集 前言&#xff1a; YOLO11于2024年9月30日由YOLOv8团队正式发布&#xff0c;为了让我们能够趁热打铁早发论文&#xff0c;接下来让我们仔细研究一下如何使用YOLO11训练自己的…

通信协议感悟

本文结合个人所学&#xff0c;简要讲述SPI&#xff0c;I2C&#xff0c;UART通信的特点&#xff0c;限制。 1.同步通信 UART&#xff0c;SPI&#xff0c;I2C三种串行通讯方式&#xff0c;SPI功能引脚为CS&#xff0c;CLK&#xff0c;MOSI&#xff0c;MISO&#xff1b;I2C功能引…

六、输入输出管理

1.输入输出程序接口 由于各种设备的操作所提供的参数或者返回值都不同&#xff0c;也很难做到以设备独立性软件向上提供统一的接口&#xff0c;但是可以将设备进行分类&#xff0c;每一类设备由一种统一的接口操作。 ①字符设备接口 get/put 系统调用:向字符设备读/写一个字符…

Redis篇(Redis原理 - RESP协议)

目录 一、简介 二、Redis通信协议 基于Socket自定义Redis的客户端 三、Redis内存回收 1. 过期key处理 1.1. 惰性删除 1.2. 周期删除 1.3. 知识小结 2. 内存淘汰策略 一、简介 Redis是一个CS架构的软件&#xff0c;通信一般分两步&#xff08;不包括pipeline和PubSub&a…

【Linux系统编程】第二十六弹---彻底掌握文件I/O:C/C++文件接口与Linux系统调用实践

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、回顾C语言文件接口 1.1、以写的方式打开文件 1.2、以追加的方式打开文件 2、初步理解文件 2.1、C文件接口 3、进一步理…

街道办事处智慧查询系统方案——未来之窗行业应用跨平台架构

一、政务公开建设的必要性 1.1.1使用便捷 人民的生活因为科学技术的发展&#xff0c;发生了翻天覆地的变化&#xff0c;也是实现智能化社区发展的重要发展环节&#xff0c;并在触摸一体机设备的运用中&#xff0c;“社区办事查询软件”运用&#xff0c;基本能够实现***科学技…

【优选算法】(第十五篇)

目录 和为k的⼦数组&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 和可被K整除的⼦数组&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 和为k的⼦数组&#xff08;medium&#xff09; 题目解析 1.题目链接&#xff1a;. - 力扣&am…

【EXCEL数据处理】000010 案列 EXCEL文本型和常规型转换。使用的软件是微软的Excel操作的。处理数据的目的是让数据更直观的显示出来,方便查看。

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000010 案列 EXCEL单元格格式。EXCEL文本型和常规型转…

TypeScript 算法手册 【归并排序】

文章目录 1. 归并排序简介1.1 归并排序定义1.2 归并排序特点 2. 归并排序步骤过程拆解2.1 分割数组2.2 递归排序2.3 合并有序数组 3. 归并排序的优化3.1 原地归并排序3.2 混合插入排序案例代码和动态图 4. 归并排序的优点5. 归并排序的缺点总结 【 已更新完 TypeScript 设计模式…

nature reviews genetics | 基因调控网络方法总结

–https://doi.org/10.1038/s41576-023-00618-5 Gene regulatory network inference in the era of single-cell multi-omics 留意更多内容&#xff0c;欢迎关注微信公众号&#xff1a;组学之心 研究团队和单位 Julio Saez-Rodriguez—Heidelberg University Ricard Arge…