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

文章目录

    • 一.什么是多级缓存?
      • 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() {// 创建本地缓存CaffinelocalCache = 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.rhkb.cn/news/434706.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Python】PyJWT:轻松实现 JSON Web Token (JWT) 网络令牌的生成与验证

PyJWT 是一个用 Python 实现的轻量级库&#xff0c;用于处理 JSON Web Token (JWT)。JWT 是一种安全的方式&#xff0c;用来表示双方之间经过签名的令牌&#xff0c;通常用于认证和授权场景。PyJWT 简化了 JWT 的生成和验证过程&#xff0c;使得开发者能够轻松地在 Python 项目…

Python | Leetcode Python题解之第443题压缩字符串

题目&#xff1a; 题解&#xff1a; class Solution:def compress(self, chars: List[str]) -> int:def reverse(left: int, right: int) -> None:while left < right:chars[left], chars[right] chars[right], chars[left]left 1right - 1n len(chars)write lef…

基于php摄影门户网站

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

Verilog:实例数组(重复实例化的快捷方法)

相关阅读 Verilog基础https://blog.csdn.net/weixin_45791458/category_12263729.html?spm1001.2014.3001.5482 前言 谈到重复实例化&#xff0c;一般都会想到for generate结构&#xff0c;但其实有一种更加简单的语法&#xff0c;只是使用的人不多&#xff0c;它就是实例数…

JavaScript 使用 Graham 扫描的凸包(Convex Hull using Graham Scan)

先决条件&#xff1a; 如何检查两个给定的线段是否相交&#xff1f; c https://blog.csdn.net/hefeng_aspnet/article/details/141713655 java https://blog.csdn.net/hefeng_aspnet/article/details/141713762 python https://blog.csdn.net/hefeng_aspnet/article/details/…

智能监控,守护绿色能源:EasyCVR在电站视频监控中心的一站式解决方案

随着科技的飞速发展&#xff0c;视频监控技术在社会安全、企业管理及智慧城市建设等领域中扮演着越来越重要的角色。特别是在电力行业中&#xff0c;电站作为能源供应的关键设施&#xff0c;其安全性和稳定性至关重要。传统的人工监控方式已难以满足现代电站复杂多变的运行需求…

基于SpringBoot+Vue+MySQL的甜品店管理系统

系统展示 用户前台界面 管理员后台界面 系统背景 在数字化浪潮的推动下&#xff0c;甜品店行业也面临着转型与升级的需求。传统的线下经营模式已难以满足现代消费者对于便捷、高效购物体验的追求。为了提升运营效率、优化顾客体验&#xff0c;我们设计了一款基于SpringBoot后端…

Ubuntu 16.04安装填坑记录

一. 问题描述&#xff1a; &#xff08;1&#xff09;Ubuntu 16.04使用USB启动盘安装时&#xff0c;出现"try ubuntu without installation"或“install ubuntu”选择&#xff0c;Enter选择安装后&#xff0c;显示器黑屏无任何显示。 原因分析&#xff1a; 显示黑…

遥感图像变换检测实践上手(TensorRT+UNet)

目录 简介 分析PyTorch示例 onnx模型转engine 编写TensorRT推理代码 main.cpp测试代码 小结 简介 这里通过TensorRTUNet&#xff0c;在Linux下实现对遥感图像的变化检测&#xff0c;示例如下&#xff1a; 可以先拉去代码&#xff1a;RemoteChangeDetection 分析PyTorch示…

网络攻击DDOoS的原理、攻击手段及防范措施详解

一、DDoS的原理 1. 原理 DDoS&#xff08;分布式拒绝服务攻击&#xff09;是利用大量的僵尸主机对受害者发起攻击&#xff0c;从而造成受害者的资源被耗尽无法为合法用户提供服务。DDoS一般采用三级结构&#xff0c;包括&#xff1a; 攻击者&#xff1a;攻击指令的发起方僵尸…

【鸿蒙HarmonyOS NEXT】数据存储之分布式键值数据库

【鸿蒙HarmonyOS NEXT】数据存储之分布式键值数据库 一、环境说明二、分布式键值数据库介绍三、示例代码加以说明四、小结 一、环境说明 DevEco Studio 版本&#xff1a; API版本&#xff1a;以12为主 二、分布式键值数据库介绍 KVStore简介&#xff1a; 分布式键值数据库…

OpenHarmony(鸿蒙南向)——平台驱动指南【DAC】

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 持续更新中…… 概述 功能简介 DAC&#xff08;Digital to Analog Converter&…

Python办公自动化之Word

在现代办公环境中&#xff0c;自动化无疑是提升工作效率的关键。特别是处理文档的工作&#xff0c;很多人可能花费大量时间在重复性任务上。那么&#xff0c;有没有一种方法可以让我们用 Python 来自动化 Word 文档的操作呢&#xff1f;今天&#xff0c;我们来聊聊如何用 Pytho…

AI Agent应用出路到底在哪?

1 Agent/Function Call 的定义 Overview of a LLM-powered autonomous agent system&#xff1a; Agent学会调用外部应用程序接口&#xff0c;以获取模型权重中缺失的额外信息&#xff08;预训练后通常难以更改&#xff09;&#xff0c;包括当前信息、代码执行能力、专有信息源…

Docker安装与应用

前言 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言开发。Docker 可以让开发者打包他们的应用以及依赖包到一个轻 量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。容器是完全使用沙箱机制&#xff0c;相互 之间…

大语言模型之LlaMA系列- LlaMA 2及LLaMA2_chat(上)

LlaMA 2是一个经过预训练与微调的基于自回归的transformer的LLMs&#xff0c;参数从7B至70B。同期推出的Llama 2-Chat是Llama 2专门为对话领域微调的模型。 在许多开放的基准测试中Llama 2-Chat优于其他开源的聊天模型&#xff0c;此外Llama 2-Chat还做了可用性与安全性评估。 …

物联网系统中基于IIC通信的数字温度传感器测温方案

01 物联网系统中为什么要使用数字式温度传感器芯片 物联网系统中使用数字式温度传感器芯片的原因主要有以下几点&#xff1a; 高精度与稳定性 高精度测量&#xff1a;数字式温度传感器芯片&#xff0c;如DS18B20&#xff0c;采用芯片集成技术&#xff0c;能够有效抑制外界不…

2024-9-28 QT登录框基础练习

1.头文件 #ifndef LOGINWINDOW_H #define LOGINWINDOW_H#include <QWidget> #include <QPushButton> #include <QLineEdit> #include <QVBoxLayout>class LoginWindow : public QWidget {Q_OBJECTpublic:// 构造函数LoginWindow(QWidget *parent nul…

打造备份一体机,群晖科技平台化战略再进阶

数字经济时代&#xff0c;海量数据不断涌现&#xff0c;并成为核心生产要素&#xff0c;驱动着企业生产方式和商业模式发生深刻变革。 与其他生产要素不同&#xff0c;数据要素具有非稀缺性、非竞争性等特征&#xff0c;且只有在具体业务场景中才能充分释放其价值。尤其是近年…

Element-Plus中上传文件upload取消提示按钮与文字

去除提示按钮与文字 添加样式&#xff0c;让这个div进行隐藏 .el-upload__input {display: none !important; }