目录
文章目录
- 目录
- 缓存
- 常见常用Java缓存技术
-
- 1.==Redis(最常用)==
- 2.==MyBatis缓存==
-
- [2.1 MyBatis一级缓存](#2.1 MyBatis一级缓存)
- [2.2 MyBatis二级缓存](#2.2 MyBatis二级缓存)
- [3.Spring Cache](#3.Spring Cache)
- 4.Ehcache
- 额外Java缓存解决方案
-
- [1.Guava Cache:](#1.Guava Cache:)
- 2.Caffeine:
- 3.OSCache:
- 4.Infinispan:
- 5.Hazelcast:
- [6.ConcurrentHashMap / LRUHashMap:](#6.ConcurrentHashMap / LRUHashMap:)
- 7.Memcached:
- [8.Apache JCS (Java Caching System):](#8.Apache JCS (Java Caching System):)
- [9.Hibernate Second Level Cache](#9.Hibernate Second Level Cache)
- 常用Java缓存技术用法
- 常用Java缓存技术总结
- 手写本地缓存Demo
- 参考文献
缓存
简介
缓存(Cache)是一种存储技术,用于临时存放从原始数据源(如硬盘、数据库或网络)获取的数据副本,目的是加快数据的访问速度,减少不必要的重复处理,进而提升系统整体的性能和响应效率。它是计算机科学中"空间换时间"策略的一个典型应用,即通过牺牲少量的存储空间来换取数据访问速度的显著提升。
简而言之,缓存就是存储数据副本或计算结果的组件,以便后续可以更快地访问。
工作原理
一句话概况:更快读写的存储介质+减少IO+减少CPU计算=性能优化。
- 数据存储:当系统首次请求数据时,数据从原始数据源加载,并被复制到缓存中。这一步通常发生在第一次访问或数据更新后重新加载时。
- 快速访问:之后,当系统再次请求相同的数据时,可以直接从缓存中获取,而无需再次访问较慢的数据源。由于缓存通常位于更快的存储介质上(如RAM),数据访问速度远高于直接从硬盘或网络获取。
- 数据更新与同步:缓存中的数据并不是永久不变的,需要有机制来维护其与数据源之间的一致性。常见的策略包括定时刷新、写穿(write-through)、写回(write-back)等。此外,为了高效利用有限的缓存空间,还会有算法(如最近最少使用LRU、最不经常使用LFU等)定期淘汰旧数据,为新数据腾出空间。
缓存分类
1.按照技术层次分类
- 硬件缓存
- CPU缓存:位于CPU和主内存之间,用于加速数据和指令的访问速度,减少CPU等待时间。
- GPU缓存:类似CPU缓存,专为图形处理单元设计,提高图形渲染和数据处理速度。
- 软件缓存
- 操作系统缓存:如文件系统缓存,用于加速文件的读写操作。
- 数据库缓存:数据库管理系统内部缓存,如MySQL的InnoDB缓冲池,缓存索引和数据页,减少磁盘I/O。
- Web应用缓存
- 浏览器缓存:存储已访问过的网页资源,如图片、样式表、JavaScript文件,加速页面加载。
- 代理服务器缓存:位于客户端和源服务器之间,存储常用网页内容,减少带宽消耗和响应时间。
2.按照应用场景分类
- 数据库查询缓存
- ORM工具缓存:如Hibernate、MyBatis的二级缓存,减少对数据库的重复查询。
- 搜索引擎缓存:加速搜索结果的呈现,如Elasticsearch的查询缓存。
- Web服务与API缓存
- RESTful API缓存:通过HTTP缓存机制(如ETag、Last-Modified)减少API响应时间。
- CDN缓存:内容分发网络,全球分布的缓存节点存储静态资源,提升访问速度和用户体验。
- 应用数据缓存
- Session缓存:存储用户的会话状态,减轻数据库负担,如Web应用中的用户登录状态。
- 计算结果缓存:如Spring Cache,用于存储昂贵计算的结果,避免重复计算。
- 分布式缓存
- Redis/Memcached:高性能键值存储系统,常用于分布式应用中共享数据缓存,提高数据访问速度和应用扩展性。
3.按照缓存策略分类
- 时间敏感型缓存
- LRU(Least Recently Used):最近最少使用策略,移除最近未被访问的数据。
- TTL(Time To Live):设置数据在缓存中的有效存活时间,到期自动删除。
- 访问频率敏感型缓存
- LFU(Least Frequently Used):最少访问次数策略,移除访问频率最低的数据。
- LFU变体(如LRU2、2Q):结合访问时间和频率进行淘汰。
- 自适应策略
- FBR(Frequency-Based Replacement) 、LRUF(Least Recently and Frequently Used First) 、ALRUF等,根据访问模式动态调整淘汰策略。
应用场景
1.硬件缓存
- CPU缓存:CPU级别的缓存分为L1、L2、L3等,用于存储频繁访问的指令和数据,减少CPU访问内存的时间。
- GPU缓存:用于存储图形处理过程中频繁访问的数据,加速图形渲染和计算任务。
2.软件缓存
数据库缓存
- 数据库查询缓存:例如MySQL的查询缓存,存储查询结果,减少对数据库的重复查询。
- ORM框架缓存:如Hibernate二级缓存、MyBatis二级缓存,提高数据访问速度。
Web开发
-
浏览器缓存:存储静态资源(如图片、CSS、JavaScript)以减少网络请求,加速页面加载。
-
Web服务器缓存:如Nginx缓存、Apache模块缓存,减少服务器对动态内容的重复生成。
-
CDN缓存:内容分发网络,全球分布存储网站内容,缩短用户访问距离,提高速度。
应用层缓存
- 会话缓存:存储用户会话信息,减轻数据库压力,如Tomcat session复制。
- 计算结果缓存:存储计算密集型操作的结果,如价格计算、推荐算法结果等。
- API响应缓存:缓存频繁请求的API响应,减少后端服务负载。
3.分布式缓存
- Redis:高可用的键值存储系统,用于存储会话信息、计数器、消息队列等。
- Memcached:简单高效的分布式缓存系统,适合存储简单的键值数据。
- Spring Cache:提供了一套抽象,方便在Spring应用中整合各种缓存技术。
4.微服务架构
- 服务间通信缓存:减少服务间重复的RPC调用,提高微服务间通信效率。
- 配置中心缓存:存储配置信息,确保配置更改时能快速传播到各服务实例。
5.移动端应用
- 离线缓存:为提高用户体验,在移动端应用中缓存数据,以便在网络不稳定或无网络时仍能访问内容。
6.大数据处理
- MapReduce缓存:在MapReduce作业中,预加载数据到内存,减少多次磁盘I/O。
- Spark缓存:将RDD(弹性分布式数据集)存储在内存中,加速迭代计算。
7.游戏开发
- 游戏资源缓存:减少游戏启动和运行时的加载时间,提升玩家体验。
通过上述场景可以看出,缓存的应用几乎覆盖了所有需要提高数据访问速度和降低系统响应时间的领域,是现代软件开发中不可或缺的技术。
缓存优点
- 提高性能:减少了对外部慢速资源的访问,提高了数据访问速度。
- 降低负载:减少了数据库、网络等资源的压力。
- 提升用户体验:页面加载更快,应用响应更迅速。
缓存带来的问题
- 数据一致性:确保缓存数据与数据源保持一致,避免脏读或数据不一致问题。
- 缓存失效:如何高效管理缓存项的生命周期,避免缓存雪崩、击穿等问题。
- 资源管理:合理分配缓存资源,避免过度占用内存。
- 缓存介质带来的不可靠性:(一般使用内存做缓存的话,若机器故障,如何保证缓存的高可用?可考虑对缓存进行分布式做成高可用,同时,需要接受这种不可靠不安全会给数据带来的问题,在异常情况下进行补偿处理,定期持久化等方式)
- 缓存的数据使得更难排查问题:因为缓存命中是随着访问随时变化的,缓存的行为难以重现,使得出现BUG很难排查。
- 进程内缓存可能会增加GC压力:在具有垃圾收集功能的语言中(如Java),大量长寿命的缓存对象会增加垃圾收集的时间和次数。
使用缓存之前我们需要对数据进行分类,对访问行为进行预估,思考哪些数据需要缓存,缓存时需要采用什么策略?这样,我们才不被缓存所困扰,才能规避这些问题。
常见常用Java缓存技术
1.Redis(最常用)
特点:Redis是一个开源的、基于键值对的数据结构存储系统,可用作数据库、缓存和消息代理。它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,且数据全部存储在内存中,提供了极高的读写速度。Redis支持主从复制和集群模式,能够满足高可用和水平扩展的需求。
应用场景:适合需要高性能、低延迟的数据缓存和实时分析场景,如社交网络的点赞计数、购物车、实时排行榜等。
实际使用:在构建实时数据分析平台时,使用Redis作为缓存存储热点数据,结合发布/订阅功能实现实时数据推送,极大提升了数据处理和响应速度。
2.MyBatis缓存
2.1 MyBatis一级缓存
特点:
- 作用域:一级缓存是SqlSession级别的缓存,即每个SqlSession实例都有自己的缓存空间。
- 生命周期:与SqlSession相同,当SqlSession关闭时,一级缓存也随之失效。
- 自动启用:MyBatis默认开启一级缓存,无需额外配置。
- 自动管理:当执行查询后,结果自动存入缓存,后续相同查询直接从缓存中读取,直到SqlSession中发生增删改操作,缓存会被清空。
- 数据隔离:不同SqlSession之间缓存不共享,适用于单线程操作或短生命周期的SqlSession。
应用场景:
- 适用于单个事务内多次查询同一数据的场景,可以减少数据库访问,提升性能。
- 适合处理简单的查询操作,尤其是在数据不频繁变动且查询频繁的情况下。
实际使用:
- 开发者通常不需要做额外配置即可享受一级缓存带来的性能提升。
- 在需要确保数据最新时,可通过手动清除缓存(如调用SqlSession的clearCache()方法)或关闭当前SqlSession来重新查询数据库。
2.2 MyBatis二级缓存
特点:
- 作用域:二级缓存是Mapper级别的,跨SqlSession共享,由同一个SqlSessionFactory创建的所有SqlSession共享。
- 配置需求:需要手动开启,且需在对应的Mapper XML文件中使用标签配置。
- 数据一致性:二级缓存更加关注数据的一致性问题,通常需要序列化和反序列化对象,且在数据更新时(增删改)会清空相关缓存。
- 灵活性:提供了更多配置选项,如缓存实现类、缓存大小、过期策略等,可以自定义缓存策略。
应用场景:
- 适合多线程环境,或在多个SqlSession间共享数据的场景。
- 对于不经常改变的共享数据,如配置信息、静态数据表等,使用二级缓存可以显著提高系统性能。
实际使用:
- 需要在MyBatis配置文件中全局启用二级缓存,并在相应的Mapper配置文件中添加标签。
- 考虑到数据一致性,使用二级缓存时要确保数据的更新策略能及时刷新缓存,避免脏读。
- 实际开发中,可能还需要结合第三方缓存工具(如Redis)进一步增强二级缓存的功能,实现更复杂的数据共享和管理。
3.Spring Cache
特点:Spring Cache是Spring框架提供的一个抽象缓存支持,它不直接提供缓存实现,而是提供了一套接口,允许开发者灵活选择和配置不同的缓存提供商,如Ehcache、Redis、Caffeine等。通过注解的方式,开发者可以轻松实现方法级别的缓存。
应用场景:适用于基于Spring框架的应用,特别是那些需要细粒度控制缓存策略的应用,如数据访问层的方法缓存、Web服务的响应缓存等。
实际使用:在构建RESTful API时,使用Spring Cache注解来缓存频繁请求但不经常变化的API响应,减少了服务器的计算和数据库查询负担,提高了服务的响应速度和吞吐量。
4.Ehcache
特点:Ehcache是一种广泛使用的、轻量级的Java进程内缓存解决方案。它支持内存和磁盘两级缓存,提供了丰富的缓存策略(如LRU、LFU等),并且可以进行分布式缓存的扩展。Ehcache配置简单,易于集成到现有的Java应用中,支持事务、监听器和统计信息收集等功能。
应用场景:适用于单机应用或小型集群环境,特别适合需要频繁读取且不经常变更的数据缓存,如用户会话、配置信息等。
实际使用:在处理高并发用户会话管理时,Ehcache可以有效减少数据库的读取压力,通过设置合理的过期策略,保证数据的新鲜度同时减轻维护负担。
额外Java缓存解决方案
1.Guava Cache:
- 特点:Guava库提供的本地缓存实现,提供了丰富的缓存过期策略(基于时间、基于大小、基于引用等)、自动加载、统计信息等功能,且易于使用,适合单机应用。
- 应用场景:适用于需要高性能、轻量级缓存解决方案的Java应用程序,特别是在处理大量数据缓存和快速查找场景下。
2.Caffeine:
- 特点:Caffeine是一个高性能、近似最近最少使用(LRU)的本地缓存库,设计上旨在超越Guava Cache,提供了更优秀的性能和灵活性。它通过优化数据结构和算法,实现了低延迟和高命中率。
- 应用场景:适用于需要高性能缓存且对响应时间敏感的应用,如高访问量的Web应用、实时数据分析系统等。
3.OSCache:
- 特点:OSCache是一个成熟的开源缓存框架,支持缓存JSP页面、Servlet输出、对象图形等,提供了事件监听器、缓存刷新机制和灵活的配置选项。
- 应用场景:虽然不如Guava和Caffeine现代,但在一些遗留系统中仍可见其身影,特别适合需要页面缓存和动态内容缓存的Web应用。
4.Infinispan:
- 特点:Infinispan是一个高度可扩展的分布式缓存框架,不仅支持内存缓存,还支持持久化存储,提供了事务支持、数据网格功能,以及丰富的数据分片和复制策略。
- 应用场景:适用于需要分布式缓存解决方案的大规模应用,特别是在需要高性能、高可用和数据一致性的场景中。
5.Hazelcast:
- 特点:Hazelcast是一个开源的内存数据网格,提供了分布式的内存缓存、计算和消息传递解决方案。它支持多节点集群、数据分区、高可用性配置等高级特性。
- 应用场景:适合需要高性能、分布式缓存和数据共享的系统,如微服务架构、大数据处理、实时分析等。
6.ConcurrentHashMap / LRUHashMap:
- 特点:虽然不是专门的缓存框架,但通过自定义实现,可以基于JDK内置的ConcurrentHashMap或继承LinkedHashMap实现简单的LRU缓存。这种方式灵活但功能相对基础。
- 应用场景:适合轻量级缓存需求,或者作为临时解决方案,特别是在对第三方依赖有严格限制的项目中。
7.Memcached:
- 特点:虽然不是纯Java实现,但通过客户端库可以在Java应用中使用。Memcached是一个高性能、分布式内存对象缓存系统,简单易用,广泛应用于缓存数据库查询结果、会话数据等。
- 应用场景:适用于跨语言环境,特别是需要在不同应用或服务之间共享缓存数据的场景。
8.Apache JCS (Java Caching System):
- 特点:JCS是一个高性能、分布式缓存系统,提供了多种缓存算法,支持多种缓存后端(如内存、磁盘、远程缓存等),并支持缓存监听器、事件通知等。
- 应用场景:适合需要灵活配置和高级缓存管理功能的应用,尤其是在需要多种缓存策略和存储介质的应用中。
9.Hibernate Second Level Cache
- 特点:作为ORM框架Hibernate的一部分,第二级缓存用于缓存数据库查询结果,以减少数据库的访问次数。它可以与多种第三方缓存实现集成,如Ehcache、Infinispan等,提供了一种透明的缓存机制,开发者无需直接操作缓存。
- 应用场景:适用于基于Hibernate的大型应用程序,尤其是那些需要频繁访问相同数据库实体的场景,通过减少数据库的读取操作,提升整体性能。
常用Java缓存技术用法
Ehcache
简介:ehcache是现在非常流行的纯java开源框架,配置简单,结构清晰,功能强大。
实际使用
1.导入依赖
xml
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
2.yml配置
yaml
spring:
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
3.ehcache的xml配置
xml
<ehcache
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<!--缓存路径-->
<diskStore path="D:/project/cache_demo/base_ehcache"/>
<!-- 默认缓存 -->
<defaultCache
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!-- helloworld1缓存 -->
<cache name="helloworld1"
maxElementsInMemory="1"
eternal="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
overflowToDisk="true"
memoryStoreEvictionPolicy="LRU"/>
<!-- helloworld2缓存 -->
<cache name="helloworld2"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="5"
timeToLiveSeconds="5"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
<!--
以下属性是必须的:
name: Cache的名称,必须是唯一的(ehcache会把这个cache放到HashMap里)。
iskStore : 指定数据存储位置,可指定磁盘中的文件夹位置
defaultCache : 默认的管理策略
maxElementsInMemory: 在内存中缓存的element的最大数目。
maxElementsOnDisk: 在磁盘上缓存的element的最大数目,默认值为0,表示不限制。
eternal: 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
overflowToDisk: 如果内存中数据超过内存限制,是否要缓存到磁盘上。
以下属性是可选的:
timeToIdleSeconds: 对象空闲时间,指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问。
timeToLiveSeconds: 对象存活时间,指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问。
diskPersistent: 是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。
diskExpiryThreadIntervalSeconds: 对象检测线程运行时间间隔。标识对象状态的线程多长时间运行一次。
diskSpoolBufferSizeMB: DiskStore使用的磁盘大小,默认值30MB。每个cache使用各自的DiskStore。
memoryStoreEvictionPolicy: 如果内存中数据超过内存限制,向磁盘缓存时的策略。默认值LRU,可选FIFO、LFU。
缓存的3 种清空策略 :
FIFO ,first in first out (先进先出).
LFU , Less Frequently Used (最少使用).意思是一直以来最少被使用的。缓存的元素有一个hit 属性,hit 值最小的将会被清出缓存。
LRU ,Least Recently Used(最近最少使用). (ehcache 默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
-->
4.编写测试代码查看使用效果
java
@Test
void ehcacheTest() {
//通过读取ehcache配置文件来创建缓存管理器即CacheManager
InputStream in = CacheDemoApplicationTests.class.getClassLoader().getResourceAsStream("ehcache.xml");
CacheManager cacheManager = CacheManager.create(in);
// 创建一个缓存实例(在配置文件中获取一个缓存实例)
final Cache cache = cacheManager.getCache("helloworld1");
final String key = "greeting";
final String key1 = "greeting1";
//创建一个数据容器来存放我们所创建的element
final Element putGreeting = new Element(key, "Hello, World!");
final Element putGreeting1 = new Element(key1, "Hello Ehcache");
//将数据放入到缓存实例中
cache.put(putGreeting);
cache.put(putGreeting1);
//取值
final Cache cache2 = cacheManager.getCache("helloworld1");
final Element getGreeting = cache2.get(key);
final Element getGreeting1 = cache2.get(key1);
// Print the value
System.out.println("value======//========"+getGreeting.getObjectValue());
System.out.println("value1=====//========"+getGreeting1.getObjectKey());
}
5.分析结果
可以从控制台中发现我们成功取到缓存值!
实现原理
Ehcache 的实现原理非常复杂,涉及多个层级的组件和逻辑,但我会尝试基于其核心组成部分和关键流程给出一个简化的概述,并尽可能结合源码细节进行解释。请注意,由于源码细节可能随版本更新而变化,以下内容基于Ehcache的一般设计思路和常见版本特性。
核心组件
- CacheManager :管理一组缓存实例的工厂类,负责创建、获取、销毁缓存。在Ehcache中,
CacheManager
是一个单例对象,可以通过CacheManager.getInstance()
获取。 - Cache :代表一个具体的缓存,包含了一系列配置项(如最大内存大小、是否溢写到磁盘等)和数据存储结构。每个
Cache
都有一个唯一的名称,并且内部维护了缓存项的集合。 - Element:缓存的基本单位,包含一个key-value对以及一些额外的元数据(如过期时间、创建时间等)。
- Store :数据存储层,包括内存存储和磁盘存储。内存存储通常是基于
SelfPopulatingCache
(自生式缓存)和MemoryStore
实现,而磁盘存储则通过DiskStore
实现。
核心流程
- 缓存初始化 :当通过
CacheManager
创建或获取一个Cache
时,首先会检查是否存在配置文件中定义的相应缓存。如果存在,则根据配置创建Cache
实例。这个过程会初始化存储结构,比如创建内存存储和配置磁盘存储策略。
java
// 简化版示例代码,非实际源码
private static CacheManager cacheManager;
static {
cacheManagerInit();
}
/**
* EhcacheConstants.CACHE_NAME, cache name
* EhcacheConstants.MAX_ELEMENTS_MEMORY, 缓存最大个数
* EhcacheConstants.WHETHER_OVERFLOW_TODISK, 内存不足时是否启用磁盘缓存
* EhcacheConstants.WHETHER_ETERNAL, 缓存中的对象是否为永久的,如果是,超过设置将被忽略,对象从不过期
* EhcacheConstants.timeToLiveSeconds, 缓存数据的生存时间;元素从构建到消亡的最大时间间隔值,只在元素不是永久保存时生效;若该值为0表示该元素可以停顿无穷长的时间
* EhcacheConstants.timeToIdleSeconds 对象在失效前的允许闲置时间,仅当eternal=false对象不是永久有效时使用;可选属性,默认值是0可闲置时间无穷大;
*/
private static CacheManager cacheManagerInit() {
if (cacheManager == null) {
//创建一个缓存管理器
cacheManager = CacheManager.create();
//建立一个缓存实例
Cache memoryOnlyCache = new Cache(EhcacheConstants.CACHE_NAME,
EhcacheConstants.MAX_ELEMENTS_MEMORY,
EhcacheConstants.WHETHER_OVERFLOW_TODISK,
EhcacheConstants.WHETHER_ETERNAL,
EhcacheConstants.timeToLiveSeconds,
EhcacheConstants.timeToIdleSeconds);
//在内存管理器中添加缓存实例
cacheManager.addCache(memoryOnlyCache);
return cacheManager;
}
return cacheManager;
}
- 缓存读取:当通过key访问缓存时,Ehcache首先在内存中查找,如果找到了对应的
Element
,则直接返回其value。如果内存中没有找到,且配置了磁盘存储和溢写策略,则尝试从磁盘加载数据。
java
public static Object getValue(String ehcacheInstanceName, Object key) {
Cache cache = cacheManager.getCache(ehcacheInstanceName);
Object value = cache.get(key).getObjectValue();
return value;
}
逻辑解释
2.1 初始化观测器并检查状态:
this.getObserver.begin();
开启获取操作的观测记录。this.checkStatus();
检查缓存当前状态是否允许执行get操作。
2.2 处理禁用状态:
- 如果缓存被标记为
disabled
,则记录观测结果为GetOutcome.MISS_NOT_FOUND
(未找到),并直接返回null,表示获取失败。
2.3 尝试从缓存中获取元素:
Element element = this.compoundStore.get(key);
试图从复合存储器(compoundStore
)中根据提供的键获取元素。
2.4 处理未找到的情况:
- 如果元素为null,说明缓存中没有对应键的值,观测记录为
GetOutcome.MISS_NOT_FOUND
,然后返回null。
2.5 检查元素是否过期:
if (this.isExpired(element)) { ... }
判断获取到的元素是否已经过期。如果过期,执行立即删除操作,并记录观测结果为GetOutcome.MISS_EXPIRED
(过期未命中),然后返回null。
2.6 更新访问统计和返回元素:
- 对于未过期的元素,如果不需要跳过访问统计更新(通过
skipUpdateAccessStatistics
判断),则调用element.updateAccessStatistics();
更新元素的访问统计信息。 - 记录观测结果为
GetOutcome.HIT
(命中),最后返回找到的元素。
- 缓存写入:写入操作首先是写入内存,然后根据配置可能还会写入磁盘。Ehcache会检查当前内存使用情况,如果超过配置的最大内存限制,可能会触发溢写到磁盘的逻辑。
java
public static void put(String ehcacheInstanceName, String key, Object value) {
Cache cache = cacheManager.getCache(ehcacheInstanceName);
cache.put(new Element(key, value));
}
逻辑解释
3.1 初始化观察者和缓存写入器:
this.putObserver.begin();
开始记录缓存插入的观测事件。- 如果
useCacheWriter
为真,则初始化缓存写入器管理器,准备调用外部的缓存写入逻辑。
3.2 状态检查与异常处理:
this.checkStatus();
检查缓存当前的状态,确保可以执行put操作。- 如果缓存被标记为
disabled
,则直接忽略此操作,并结束观测。
3.3 处理空元素:
- 如果传入的
element
为null,根据doNotNotifyCacheReplicators
条件输出日志提示(可能是因为软引用被垃圾回收),然后结束观测,不执行任何插入操作。
3.4 验证元素有效性:
- 检查元素的键是否为null,若为null,则同样忽略操作并结束观测。
3.5 元素预处理:
- 重置元素的访问统计信息。
- 若元素未设置生命周期(lifespan),应用默认配置。
- 如果磁盘空间已满,执行退避逻辑(可能包括等待、抛出异常等)。
3.6 执行缓存写入:
- 根据
useCacheWriter
标志,决定是否使用缓存写入器进行操作。如果使用,调用compoundStore.putWithWriter
,该方法会先尝试调用外部定义的CacheWriter
(如果配置了的话),然后再执行实际的存储操作。 - 在这个过程中,如果遇到
StoreUpdateException
异常,会根据配置决定是否通知监听器,并重新抛出异常。 - 如果不使用缓存写入器,则直接调用
compoundStore.put
进行存储。
3.7 更新访问统计与通知:
- 更新元素的更新统计信息。
- 根据元素是否已存在于缓存中(即是否是更新操作而非新增),调用
notifyPutInternalListeners
通知相关的监听器。
3.8 结束观测并返回结果:
- 根据元素是否已存在,记录插入操作的结果(
ADDED
或UPDATED
)并结束观测。
- 缓存淘汰策略 :当内存达到上限时,Ehcache会根据配置的淘汰策略(如LRU、LFU)来决定哪些
Element
应该被淘汰出内存。这一过程可能涉及复杂的算法和数据结构管理。 - 缓存清理与过期:Ehcache支持定时清理过期的缓存项,这通常由后台线程周期性执行。同时,每次读取或写入操作也会检查元素是否过期。
深入理解Ehcache的实现原理,需要查看具体版本的源码,特别是net.sf.ehcache.Cache
、net.sf.ehcache.store.MemoryStore
、net.sf.ehcache.store.DiskStore
等类。例如,内存存储的管理在MemoryStore
中实现,其中可能包含一个HashMap
或者更复杂的数据结构来维护缓存项。磁盘存储则涉及文件系统的读写操作,DiskStore
类中会有相关的文件操作逻辑。
Redis
简介
Redis是我们平常开发中最常用到的缓存中间件了!Redis是一个开源的、高性能的键值存储系统,它以高效的数据结构存储数据,并支持多种数据类型,包括字符串(Strings)、哈希(Hashes)、列表(Lists)、集合(Sets)、有序集合(Sorted Sets)等。由于它的超高性能,使其成为我们在开发中首选的缓存工具!
核心特性
- 高性能 :Redis将数据
存储在内存
中,使用C语言实现
,单线程模型
减少了线程上下文切换的开销,因此能够提供非常高的读写速度。官方数据表明,Redis能够支持每秒十万次以上的读写操作。 - 数据持久化:Redis提供了两种数据持久化机制------RDB(快照)和AOF(追加文件)。RDB通过定时快照的方式将内存中的数据保存到磁盘上,适合恢复和备份;AOF则记录每次写操作,保证数据的高完整性,但可能会占用更多磁盘空间。
- 网络IO模型:Redis使用单线程模型处理网络请求,通过非阻塞IO和多路复用(如epoll在Linux上的实现)来高效地处理并发连接,避免了多线程锁的竞争开销。
- 多种数据结构:Redis不仅仅是一个简单的键值存储,它支持丰富多样的数据结构,这使得它能灵活地应用于多种场景,如计数器、排行榜、消息队列等。
- 发布/订阅功能:Redis支持发布/订阅模式,可以作为消息队列使用,实现消息的广播和订阅,适用于实时通知、聊天系统等。
- 事务:尽管不是严格的ACID事务,Redis提供了简单的事务支持,允许一系列操作在没有其他客户端请求干扰的情况下连续执行。
- Lua脚本:Redis支持在服务器端执行Lua脚本,可以用来实现复杂的逻辑处理,减少网络往返,提高执行效率。
- 主从复制与高可用:Redis支持主从复制,可以设置多个从节点,提高系统的可用性和数据安全性。在此基础上,还可以配置哨兵模式或集群模式,实现自动故障转移,进一步提高系统的高可用性。
- 模块化扩展:Redis支持模块化扩展,允许开发者为Redis增加新的数据类型和功能,进一步增强了其灵活性和功能性。
应用场景
Redis因其独特的特性和高性能,被广泛应用于缓存、会话存储、实时分析、消息队列、排行榜、计数器、社交网络、游戏开发等多个领域。在许多情况下,它作为数据库的补充,加速数据访问,减少数据库压力,提高整体系统的响应速度。
实际使用
1.导入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.对Redis进行配置
java
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//默认的Key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化
//redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
3.启动类上开启注解
4.使用时注入RedisTemplate
java
@Resource
private RedisTemplate redisTemplate;
5.在接口中使用
-
比如我们在查询数据时先去查询缓存,如果查不到再去查询数据库,并存入Redis中,下一次就可以直接命中缓存
java@ApiOperation("分段提示展示") @GetMapping("/show") public R<List<ScoreNote>> show(@RequestParam("questionListId") String questionListId) { List<ScoreNote> scoreNoteList = null; // 动态构造key String key = "score_note_" + questionListId; // 例:score_note_1397844391040167938 // 先从Redis中获取缓存数据 scoreNoteList = (List<ScoreNote>) redisTemplate.opsForValue().get(key); if (scoreNoteList != null) { // 如果存在, 直接返回, 无需查询数据库 return R.success(scoreNoteList); } LambdaQueryWrapper<ScoreNote> wrapper = new LambdaQueryWrapper<ScoreNote>() .eq(ScoreNote::getQuestionListId, questionListId) .orderByAsc(ScoreNote::getEndScore); scoreNoteList = scoreNoteService.list(wrapper); // 将查询到的fen'duan数据缓存到Redis中 redisTemplate.opsForValue().set(key, scoreNoteList, 1, TimeUnit.HOURS); return R.success(scoreNoteList); }
-
同时,在增删改时要记得删除缓存,防止缓存和数据库不一致
java@ApiOperation("删除分段提示") @PostMapping("/delete") public R<String> delete(@RequestBody ScoreNote scoreNote) { if (isNullOrEmpty(scoreNote.getId())) { return R.error("无效参数"); } scoreNoteService.removeById(scoreNote.getId()); // 清理所有分段提示的缓存数据 Set keys = redisTemplate.keys("score_note_*"); redisTemplate.delete(keys); return R.success("删除成功"); }
6.效果展示
-
可以看到数据是正常返回的
-
第二次请求也是直接命中了缓存
-
再来看我们Redis的存储情况,也是成功存储!
注意事项
尽管Redis提供了诸多优点,但在使用时也需要注意其内存消耗,因为所有数据默认存储在内存中。此外,对于大规模数据存储和高并发访问,需要合理规划内存使用、持久化策略和集群部署,以确保系统的稳定性和性能。
实现原理
简述
Redis 实现原理涉及以下几个核心方面:
- 事件驱动模型:Redis 核心是一个事件循环(event loop)模型,它基于 Reactor 模式。程序围绕一个事件循环进行,通过 I/O 多路复用(如Linux下的epoll或select)来监听和处理多个客户端连接。当有新的连接请求或已有连接上有数据可读写时,事件循环会将其加入到待处理事件队列,并分配给事件处理器执行相应的操作。这样,Redis 能够在一个单线程内高效地处理大量的并发连接。
- 内存数据存储:Redis 将所有数据存储在内存中,这大大减少了数据访问延迟,使得读写操作极为迅速。内存响应时间大约为100纳秒级别,这是Redis能够达到每秒百万级甚至更高访问量的基础。数据结构包括字符串(Strings)、列表(Lists)、哈希(Hashes)、集合(Sets)、有序集合(Sorted Sets)、位图(Bitmaps)、超日志(LogLogs)、地理空间索引(GeoSpatial Indexes)等,每种数据结构都有优化的内部实现,以提高存储和访问效率。
- 单线程模型:Redis 采用单线程模型处理客户端请求,避免了多线程环境下的上下文切换开销和锁竞争问题。单线程简化了编程模型,使得代码更容易理解和维护,同时也降低了错误的可能性。尽管单线程可能看似限制了CPU的使用,但Redis的瓶颈通常在于网络IO和内存访问速度,而非CPU计算能力。
- 持久化机制:为了防止数据丢失,Redis 提供了两种持久化方式:RDB(快照)和AOF(追加文件)。RDB是定时将内存中的数据保存到磁盘上的二进制文件,适合做备份;AOF则是记录每一条写命令,持续追加到文件中,保证更高的数据安全性,但会占用更多磁盘空间。Redis 还支持混合使用这两种方式,以平衡数据安全性和性能。
- 集群与分布式:Redis Cluster 提供了数据自动分区和故障转移的能力,允许数据分布在多个节点上。每个节点都持有数据的一部分,并且能够透明地处理节点加入、离开或失败的情况。Redis Sentinel 系统用于监控集群中各个节点的状态,当主节点出现问题时,Sentinel 可以自动进行故障转移,提升系统的可用性。
- 网络通信:早期版本的Redis基于自己定制的事件处理逻辑,而在新版本中,Redis开始支持更多的网络通信库,如libuv、asyncio等,这些库进一步提升了网络通信的效率和跨平台兼容性。
SDS(Simple Dynamic String)
- Redis使用SDS而非C语言标准字符串,以提供快速的长度获取(O(1)复杂度)、自动扩容、空间预分配和惰性释放等优势。
- SDS设计包括三个核心部分:
free
(空闲空间)、len
(内容长度)、buf
(存储内容的字符数组),支持二进制数据存储。
List的实现
- Redis 3.2前,List可使用ziplist或linkedlist实现,之后引入了QuickList。
- ziplist是一种紧凑存储结构,适用于元素数量少、元素小的情景,优点是内存连续,缺点是插入删除操作可能引发元素移动。
- linkedlist采用双向链表结构,提供了良好的插入删除性能。
- QuickList结合两者优点,将多个ziplist通过双向链表相连,以适应大数据量存储,同时保持了ziplist的内存高效性。
Set和Zset的实现
- Set根据条件选择整数集合或字典实现,前者用于整数且数量不大于特定阈值的集合。
- Zset(有序集合)根据元素数量和大小选择ziplist或SkipList实现,ziplist用于数据量小且元素长度受限的场景,而SkipList(跳跃表)提供了对大量数据高效排序和检索的能力,平均查找复杂度为O(logN)。
Hash的实现
- Hash也采用ziplist或hashtable(哈希表)实现,具体依据是元素个数和大小。
- 当元素数量少、大小符合配置条件时,使用ziplist以节省空间;反之,使用哈希表保证快速访问。
总结
Redis内部几种数据结构的底层实现原理,通过巧妙的设计平衡内存使用、访问速度和数据操作效率。从SDS的灵活高效,到List、Set、Zset、Hash根据不同条件采用不同存储结构,展现了Redis在实现高性能缓存存储方面的技术细节。这些机制共同支撑起Redis作为一个快速、灵活且强大的数据存储解决方案的基础。
Spring Cache
简介
Spring Cache 是 Spring 框架中一个强大的模块,它为开发者提供了一套基于注解的缓存抽象。这一抽象层使得开发者能够在不修改太多代码的情况下,方便地将缓存逻辑集成到应用程序中,从而提升应用的性能和响应速度。Spring Cache 的设计目标是简化缓存的使用,同时保持足够的灵活性以支持多种缓存实现。
我们日常开发中经常会将Spring Cache和Redis结合使用,达到既开发代码简洁(可以直接使用注解开发),又可以实现缓存的高性能!
主要特点
- 基于注解的配置 :Spring Cache 使用注解(如
@Cacheable
,@CachePut
,@CacheEvict
,@Caching
)来声明缓存策略,这些注解可以直接应用在方法上,减少了手动管理缓存的复杂性。 - 缓存提供者无关性:Spring Cache 提供了一层抽象,使得开发者可以无缝切换不同的缓存实现,如 Ehcache、Redis、Caffeine、 Hazelcast 等。只需更改配置,无需修改业务代码。
- AOP 支持:Spring Cache 利用了面向切面编程(AOP)的技术,自动拦截标记了缓存注解的方法调用,根据注解的指示进行缓存的读取或更新,减少了对业务逻辑的侵入。
- 灵活的缓存管理:支持多种缓存策略,如缓存过期、缓存更新策略(如写入时更新、读取时更新)、缓存淘汰策略等,可以根据应用需求进行细粒度的配置。
- 事务感知:Spring Cache 能够与 Spring 的事务管理集成,确保缓存操作与数据库事务的一致性。
常用注解
- @Cacheable:标记在方法上,表示该方法的返回结果应该被缓存起来,下次相同参数的调用可以直接从缓存中获取结果。
- @CachePut:同样标记在方法上,当方法被执行时,其结果将会被放入缓存,但它不会影响方法的正常执行,即使缓存操作失败也不会抛出异常。
- @CacheEvict:用于缓存的清除操作,可以配置在方法上,当方法被调用时,根据配置的规则清除缓存项。
- @Caching:当需要在一个方法上应用多个缓存操作时,可以使用此注解组合多个缓存操作。
实际使用
1.导入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2.同样需要启动类开启注解
java
@EnableCaching // 开启Spring Cache注解方式缓存功能
3.直接在接口中进行使用
-
比如我们在新增和删除时可以进行缓存清除,直接加注解就可以实现
java@ApiOperation("添加文章") @PostMapping("/add") @CacheEvict(value = "articleCache", allEntries = true) // 清除所有缓存 public R<String> add(@RequestBody Article article) { if (isNullOrEmpty(article.getCategoryId(), article.getTitle(), article.getContent())) { return R.error("无效参数"); } articleService.save(article); return R.success("添加成功"); } @ApiOperation("删除文章") @PostMapping("/delete") @CacheEvict(value = "articleCache", allEntries = true) // 清除所有缓存 public R<String> delete(@RequestBody Article article) { if (isNullOrEmpty(article.getId())) { return R.error("无效参数"); } articleService.removeById(article.getId()); return R.success("删除成功"); }
-
我们在修改时可以进行缓存清除,也可以进行重新放入缓存(推荐,粒度更小),下面是两个注解进行实现
java@ApiOperation("修改文章") @PostMapping("/update") // @CacheEvict(value = "articleCache", allEntries = true) // 清除所有缓存 @CachePut(value = "articleCache", key = "#articleReq.id") // 将结果放入缓存 public R<String> update(@RequestBody Article articleReq) { if (isNullOrEmpty(articleReq.getId())) { return R.error("无效参数"); } Article article = articleService.getById(articleReq.getId()); if (article == null) { return R.error("文章不存在"); } BeanUtils.copyProperties(articleReq, article); articleService.updateById(article); return R.success("修改成功"); }
-
查询时是一样的逻辑:先查询缓存,查不到再去查数据库并放入缓存,这里我们通过注解可快速实现
java@ApiOperation("查询文章列表") @GetMapping("/list") @Cacheable(value = "articleCache", key = "#categoryId") // 把返回的文章数据进行缓存 public R<Page<Article>> list(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum, @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize, @RequestParam(value = "categoryId") String categoryId, @RequestParam(value = "ss", required = false) String ss) { LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<Article>() .eq(Article::getCategoryId, categoryId) .orderByDesc(Article::getCreateTime); if (!isNullOrEmpty(ss)) { queryWrapper.like(Article::getTitle, ss); } Page<Article> page = new Page<>(pageNum, pageSize); articleService.page(page, queryWrapper); return R.success(page); } @ApiOperation("查询文章详情") @GetMapping("/detail") @Cacheable(value = "articleCache", key = "#id") // 把返回的文章数据进行缓存 public R<ArticleResp> detail(@RequestParam(value = "id") String id) { if (isNullOrEmpty(id)) { return R.error("无效参数"); } Article article = articleService.getById(id); if (article == null) { return R.error("文章不存在"); } ArticleResp articleResp = new ArticleResp(); BeanUtils.copyProperties(article, articleResp); // 设置点赞数 int count = likeService.count(new LambdaQueryWrapper<Like>() .eq(Like::getEventId, id)); articleResp.setLikeNum(count); // 设置评论列表 List<CommentResp> commentRespList = commentService.list(new LambdaQueryWrapper<Comment>() .eq(Comment::getEventId, id)) .stream() .map(comment -> { CommentResp commentResp = new CommentResp(); BeanUtils.copyProperties(comment, commentResp); User user = userService.getById(comment.getUserId()); if (user != null) { commentResp.setUser(user); } // 设置评论点赞数 commentResp.setLikeNum(likeService.count(new LambdaQueryWrapper<Like>() .eq(Like::getEventId, comment.getId()))); // 设置评论回复列表 List<Comment> commentList = commentService.list(new LambdaQueryWrapper<Comment>() .eq(Comment::getEventId, comment.getId())); if (!commentList.isEmpty()) { List<CommentResp> commentARespList = commentList.stream().map(commentA -> { CommentResp commentAResp = new CommentResp(); BeanUtils.copyProperties(commentA, commentAResp); User user1 = userService.getById(comment.getUserId()); if (user1 != null) { commentAResp.setUser(user1); } // 设置评论点赞数 commentAResp.setLikeNum(likeService.count(new LambdaQueryWrapper<Like>() .eq(Like::getEventId, commentA.getId()))); return commentAResp; }).collect(Collectors.toList()); commentResp.setCommentRespList(commentARespList); } return commentResp; }).collect(Collectors.toList()); articleResp.setCommentRespList(commentRespList); return R.success(articleResp); }
4.效果展示
小插曲:注意在使用Spring Cache时,使用的缓存要唯一。比如使用Redis,就不要配置Ehcache。同样,使用Ehcache,就不要配置Redis。否则将会抛出异常,Spring容器会找不到你所存储的缓存配置。这里就是因为我两个都进行了配置,也是解决了很久!!!
-
正常返回查询结果
-
Redis中也进行了正常存储
-
对本文章进行修改
-
可以看到,本文章也进行了正常缓存
总结
综上所述,Spring Cache 提供了一种简洁、高效的方式来集成缓存机制,帮助开发者构建高性能的应用程序,同时保持了代码的清晰和可维护性。
实现原理
简述
Spring Cache 是Spring框架提供的一种抽象缓存机制,它允许开发者在不改变原有业务逻辑的情况下,通过简单的注解配置来实现对方法调用结果的缓存,从而提高应用的性能。Spring Cache的核心实现原理基于Spring AOP
(面向切面编程)和模板方法模式
。
核心组件
- Spring AOP (面向切面编程)
Spring Cache利用AOP拦截标记了特定注解(如@Cacheable
, @CachePut
, @CacheEvict
等)的方法调用。当一个方法被调用时,Spring的代理(基于JDK动态代理或CGLIB)会先于目标方法执行之前和之后插入缓存逻辑。
- 缓存管理器 (Cache Manager)
CacheManager
是Spring Cache的核心组件,负责创建和管理各种缓存实例。它是一个接口,Spring提供了多种实现,比如ConcurrentMapCacheManager
(使用Java的ConcurrentHashMap作为缓存存储)、EhCacheCacheManager
(集成Ehcache)、RedisCacheManager
(集成Redis)等。开发者可以根据需要选择合适的缓存实现。
- 缓存注解
@Cacheable
: 用于方法上,表示方法的返回结果应该被缓存起来,下次相同的调用直接从缓存中获取,而不是执行实际方法。@CachePut
: 同样用于方法上,但与@Cacheable
不同,它在方法执行后无论结果如何都会更新缓存,常用于数据更新操作。@CacheEvict
: 用于方法上,用于清除缓存中的某些项,可以指定清除条件。@Caching
: 允许在一个方法上组合多个缓存操作注解。
- 缓存解析器 (Cache Resolver) 和缓存解析策略 (Cache Resolution Strategy)
这些组件负责确定在执行缓存操作时使用哪个具体的缓存管理器和缓存。Spring提供了默认实现,但也可以自定义以适应特定需求。
工作流程
- 方法调用前 :AOP代理检测到方法上存在缓存注解,如
@Cacheable
。 - 解析缓存键:根据注解配置和方法参数,使用Spring Expression Language (SpEL) 解析出缓存键。
- 查找缓存:通过缓存管理器和解析出的键在缓存中查找结果。
- 缓存命中:如果找到,直接返回缓存中的结果,跳过方法执行。
- 缓存未命中 :执行方法,获取结果,并根据
@Cacheable
或@CachePut
注解的配置将结果放入缓存。 - 方法执行后 :如果是
@CacheEvict
,根据配置清除相关缓存项。
通过这种机制,Spring Cache能够无缝集成到Spring应用中,提供了声明式缓存策略,大大简化了缓存逻辑的实现,提高了应用程序的响应速度和性能。
MyBatis缓存
简介
MyBatis缓存我们平常一般不会去特意留意或者去开启,但是MyBatis的缓存几乎是我们接触到最多的Java缓存技术!MyBatis 提供了两层缓存机制,旨在通过减少数据库访问次数来提升查询性能。MyBatis会默认帮我们开启一级缓存!
一级缓存(本地缓存 / Session 级别缓存)
- 默认开启,是线程级别的缓存,也称为SqlSession的缓存。
- 在一个SqlSession生命周期中有效,当SqlSession关闭时,缓存会被清空。
- 在同一个SqlSession中,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和结果存放在一个Map中。如果后续的键值一样,则直接从Map中获取数据。
- 不同的SqlSession之间的缓存是相互隔离的。
- 缓存是以namespace为单位的,不同namespace下的操作互不影响。
- 当执行UPDATE、INSERT、DELETE语句时,可以通过配置使得在查询前清空缓存(通过设置flushCache="true")。
作用域:一级缓存是SqlSession级别的,意味着它只在一个SqlSession的生命期内有效。当一个SqlSession被创建时,MyBatis会为其分配一个缓存空间,该缓存仅对该SqlSession可见。同一SqlSession内的多次相同查询,第二次及以后的查询会直接从缓存中获取结果,避免了重复的数据库访问。
触发条件:只要同一个SqlSession执行相同的Mapper方法和参数,就会命中一级缓存。
失效情况:
- 当SqlSession执行commit、rollback、close操作时,一级缓存会被清空。
- 执行了任何更新操作(如insert、update、delete)后,也会清空缓存,因为数据可能已经改变。
- 显式调用
clearCache()
方法也会清空缓存。
一级缓存的工作原理
- 缓存存储:当在一个SqlSession中执行查询操作时,MyBatis会将查询结果存储在内部的一个Map对象中。这个Map的键是查询的SQL语句和参数,值是查询结果。
- 缓存命中:如果在同一个SqlSession中再次执行相同的查询(即SQL语句和参数都相同),MyBatis会首先检查一级缓存中是否存在该查询的结果。如果存在,则直接从缓存中获取结果,而不是再次访问数据库。
- 缓存失效:一级缓存的生命周期与SqlSession相同。当SqlSession关闭或提交/回滚事务时,一级缓存中的数据将被清空。此外,如果在同一个SqlSession中执行了更新操作(如INSERT、UPDATE、DELETE),那么一级缓存也会被清空,以确保数据的一致性。
一级缓存的优缺点
- 优点:由于一级缓存是SqlSession级别的,因此它提供了非常快的查询速度。对于频繁执行的相同查询,一级缓存可以显著减少数据库的访问次数。
- 缺点:一级缓存的缺点是它的生命周期较短,且只能在一个SqlSession内部使用。因此,如果需要在多个SqlSession之间共享数据,就需要使用二级缓存。
二级缓存(全局缓存 / SqlSessionFactory 级别缓存)
- 二级缓存则是为了延长查询结果的保存时间,从而提高系统性能。它也可以用于共享数据。
- 二级缓存的范围更大,它可以跨SqlSession,是多个SqlSession共享的。
- MyBatis允许你在映射文件中配置
<cache/>
标签以开启二级缓存。当这个映射文件被加载时,会创建一个Cache实例,并将其存储在MapperRegistry中。当一个新的SqlSession执行查询时,会先检查这个MapperRegistry中是否存在对应的Cache实例,如果存在则使用这个Cache实例进行查询。
作用域:二级缓存是基于SqlSessionFactory的,跨SqlSession共享,适用于分布式环境。一旦开启,同一个命名空间下的SQL查询结果会被存储在二级缓存中,后续的SqlSession如果执行相同的查询语句,就可以直接从二级缓存中获取数据。
配置与启用 :二级缓存需要在MyBatis的配置文件中启用,并且在需要缓存的Mapper XML文件中指定<cache/>
元素。同时,实体类必须实现序列化接口(Serializable),以便在不同线程和进程中共享。
失效策略:二级缓存可以配置过期时间、内存大小限制等,以及使用LRU(最近最少使用)等策略来管理缓存空间。
注意事项:
- 二级缓存对数据一致性要求较高,如果数据更新频繁,需要考虑缓存与数据库数据同步的问题。
- 因为二级缓存在多个SqlSession间共享,所以对数据的修改需要考虑如何刷新缓存,以确保缓存数据的时效性和一致性。
- 二级缓存的粒度通常是Mapper级别的,即一个Mapper下的所有查询都可以共享缓存,但可以通过
<cache-ref/>
元素来引用另一个Mapper的缓存配置,实现更细粒度的控制。
二级缓存的工作原理
- 缓存存储:当某个Mapper的查询结果被存储到二级缓存后,其他Mapper或SqlSession也可以访问这些数据。与一级缓存类似,二级缓存也使用Map来存储数据,但Map的键可能更加复杂,以区分不同的Mapper和查询条件。
- 缓存命中:当其他Mapper或SqlSession执行相同的查询时,MyBatis会首先检查二级缓存中是否存在该查询的结果。如果存在,则直接从缓存中获取结果,而不是再次访问数据库。
- 缓存失效:二级缓存的失效机制与一级缓存类似。当执行更新操作时,相关的二级缓存会被清空。此外,还可以通过配置来设置缓存的过期时间等参数。
实际使用
我们平常开发过程中通过对二级缓存的配置来进行使用!
在配置MyBatis的缓存时,有两种主要的方法:
- 在全局配置文件中配置缓存:在MyBatis的全局配置文件中,通过
<settings>
元素的子元素<setting>
来配置缓存,可以设置缓存的类型和其他相关属性。 - 在注解中配置缓存:在使用注解的方式进行SQL映射时,可以使用
@CacheNamespace
注解来配置缓存。
以MyBatis-Plus为例:
-
不开启的情况下:
可以发现,如果我们不开启二级缓存,即使是同样的SQL也不会命中缓存!!!
-
开启二级缓存
-
在application.yml或application.properties中开启缓存
对于
application.yml
,添加如下配置:yamlmybatis-plus: configuration: cache-enabled: true
如果使用
application.properties
,则配置为:propertiesmybatis-plus.configuration.cache-enabled=true
-
在需要使用二级缓存的Mapper接口上添加
@CacheNamespace
注解。如果你希望自定义缓存行为,可以在该注解中设置更多属性,如指定缓存的实现类等。
-
-
再次测试缓存是否命中:
可以看到确实是命中了缓存!SQL也是只执行了一次!
实现原理
MyBatis的二级缓存是建立在SqlSessionFactory级别上的,旨在提高应用程序的整体性能,通过跨SqlSession重用已经执行过的查询结果。其实现原理概括如下:
- 缓存作用域:二级缓存的范围是基于命名空间(Namespace)的,这意味着,同一个命名空间下的SQL查询结果会被共享。每个Mapper映射文件都有一个独立的命名空间,MyBatis会为每个命名空间创建一个单独的二级缓存。
- CacheExecutor装饰模式 :当MyBatis配置开启二级缓存(
cacheEnabled=true
),在创建SqlSession时,MyBatis会使用CacheExecutor
来装饰原始的Executor(执行器)。CacheExecutor
是一个装饰器模式的应用,它会在执行查询之前先检查二级缓存中是否存在所需数据。 - 缓存存储结构:二级缓存的数据结构是基于Map的,其Key由Mapper的ID、查询条件(如偏移量、限制条件、SQL语句以及所有输入参数)组成,Value是查询结果。这样的设计允许MyBatis根据精确的查询条件快速定位缓存数据。
- 缓存失效策略:当执行增删改操作提交事务后,MyBatis会清空相关的二级缓存,以保证数据一致性。此外,还可以通过配置自定义的缓存同步策略,例如基于时间或基于某些条件的自动刷新。
- 自定义缓存实现 :MyBatis允许开发者通过实现
org.apache.ibatis.cache.Cache
接口来自定义缓存实现。这为集成第三方缓存系统(如Redis、Memcached)提供了可能。 - 启用条件 :为了启用二级缓存,除了全局配置外,还需要在每个Mapper的XML文件中使用
<cache>
标签,并可以配置缓存的属性,如缓存实现类、序列化策略等。 - 缓存交互过程 :
- 当执行查询时,
CacheExecutor
首先检查二级缓存中是否存在匹配的键值对。 - 如果存在,则直接从缓存中返回结果,避免了数据库查询。
- 如果不存在,则执行查询并将结果存储到二级缓存中,以便后续相同查询能够直接命中缓存。
- 当执行查询时,
MyBatis的二级缓存通过在执行器层面添加一层缓存逻辑,利用Map存储查询结果,并基于命名空间进行隔离,实现了跨SqlSession的数据共享,有效减少数据库访问次数,提升了应用性能。
常用Java缓存技术总结
Ehcache
概述: Ehcache 是一个纯Java编写的、广泛应用于Java平台的进程内缓存框架,它提供了内存和磁盘存储、缓存加载器、缓存扩展、缓存异常处理器等特性。Ehcache特别适合于单机应用或者小型集群环境,因为它直接运行在JVM中,访问速度快,效率高。但是,对于大规模分布式系统,Ehcache在缓存共享和集群部署上的支持不够完善,多个节点间的数据同步较为困难。
使用场景:
- 单个应用或小型项目,对缓存访问速度要求极高的场合。
- 作为MyBatis等ORM工具的二级缓存,提高数据库查询性能。
- 在Shiro等安全框架中存储会话信息,提升应用安全性与性能。
Redis
概述: Redis是一个开源的、基于键值对的数据结构存储系统,可以用作数据库、缓存和消息中间件。它支持多种数据结构如字符串、哈希、列表、集合、有序集合等,并且提供了丰富的API。Redis运行在独立的服务器上,客户端通过网络协议访问,因此它天然支持分布式缓存,非常适合大型分布式系统。
使用场景:
- 大型分布式系统,尤其是需要跨服务共享缓存的场景。
- 对数据持久化有要求的缓存应用,Redis支持数据持久化到硬盘。
- 实现复杂的缓存策略,如排行榜、计数器、发布/订阅等高级特性。
- 高并发环境下的会话缓存、全页缓存、消息队列等。
Spring Cache
概述 : Spring Cache是Spring框架提供的一个抽象缓存管理接口,它允许开发者在应用中轻松地集成各种缓存技术,如Ehcache、Redis等。Spring Cache通过AOP(面向切面编程)技术,对标注了缓存注解(如@Cacheable
, @CachePut
, @CacheEvict
)的方法进行拦截,从而实现缓存逻辑的透明化。
使用场景:
- 任何基于Spring的项目,特别是需要缓存策略来提升性能的应用。
- 当需要灵活切换缓存实现时,Spring Cache的抽象层提供了良好的兼容性和可替换性。
- 在需要细粒度缓存控制,如缓存过期策略、条件缓存、缓存清除等复杂场景中。
- 结合Spring Boot快速搭建带有缓存功能的微服务架构。
MyBatis缓存
概述: MyBatis提供了两层缓存机制:一级缓存(本地缓存)和二级缓存(全局缓存)。一级缓存是在SqlSession级别,生命周期短暂,主要用于同一个SqlSession内的查询优化。二级缓存则是跨SqlSession的,位于命名空间级别,可以配置成跨节点的分布式缓存,提高数据库查询的复用率。
使用场景:
- 一级缓存适用于短周期、高频率的查询操作,减少同一SqlSession内的重复查询。
- 二级缓存适用于多用户、多会话场景,特别是在查询结果相对稳定、不经常变更的数据表上,可以大幅度提高应用性能。
- 当应用需要跨服务共享查询结果,或在分布式环境下减少数据库负载时,配置分布式二级缓存(如使用Redis作为二级缓存)尤为重要。
小结
总结来说,Ehcache和Redis在缓存实现上有明显区别,前者更适合单机或小规模环境,后者则在分布式和大数据量场景下表现更优。Spring Cache作为Spring生态的一部分,提供了统一的缓存抽象,使得在Spring应用中集成缓存变得更加便捷。而MyBatis的缓存机制,作为ORM框架的内置功能,主要关注数据库查询结果的高效复用,提升数据访问性能。在实际应用中,根据具体需求和场景,可以选择合适的缓存策略和技术组合。
对于本次Java缓存技术的收集和报告,实实在在让我更加深刻的理解和深入了解了缓存。我也相信一定会对自己的编程生涯产生深远影响。还有很多种Java缓存技术并没有进行更加深入的了解,日后我也会结合日常工作或新场景下对其他缓存进行学习,争取能够将大部分缓存技术得心应手应用在项目中!
手写本地缓存Demo
Demo
本DemoLocalCache
类设计实现了丰富的功能,包括自动过期、容量限制、并发控制以及定期清理机制。
java
@Slf4j
public class LocalCache<K, V> implements Map<K, V> {
/**
* 缓存默认最大容量: 4096
*/
private static final int DEFAULT_MAX_CAPACITY = 1 << 12;
/**
* 定期全面清理过期数据的间隔时间: 1分钟
*/
private static final int ONE_MINUTE = 60 * 1000;
/**
* 最大容量
*/
private final int maxCapacity;
/**
* 默认过期时间(毫秒), -1 表示不过期.
*/
private long defaultTtlTime = -1;
/**
* 缓存使用频率
*/
private final LinkedList<K> cacheList = new LinkedList<>();
/**
* 缓存对象
*/
private final ConcurrentHashMap<K, CacheValue<V>> cacheMap;
public LocalCache() {
this(DEFAULT_MAX_CAPACITY);
}
/**
* @param maxCapacity 最大容量
*/
public LocalCache(int maxCapacity) {
this(maxCapacity, -1L);
}
/**
* @param maxCapacity 最大容量
* @param defaultTtlTime 默认过期时间(毫秒), -1 表示不过期.
*/
public LocalCache(int maxCapacity, long defaultTtlTime) {
this.maxCapacity = maxCapacity;
if (defaultTtlTime > 0) {
this.defaultTtlTime = defaultTtlTime;
}
cacheMap = new ConcurrentHashMap<>();
Thread thread = new Thread(this::cleanUpExpired, "clear_thread");
thread.setDaemon(true);
thread.start();
}
/**
* 返回缓存中有效键值的数量
*/
@Override
public int size() {
return entrySet().size();
}
/**
* 返回缓存中是否有 有效键值数据
*/
@Override
public boolean isEmpty() {
return entrySet().isEmpty();
}
/**
* 返回缓存中是否有 有效的此键
*/
@Override
public boolean containsKey(Object key) {
CacheValue<V> cacheValue = cacheMap.get(key);
if (cacheValue != null) {
if (cacheValue.ttlTime == -1 || cacheValue.ttlTime > System.currentTimeMillis()) {
return true;
}
remove(key);
return false;
}
return false;
}
/**
* 返回缓存中是否有 有效的此值
*/
@Override
public boolean containsValue(Object value) {
long now = System.currentTimeMillis();
for (Entry<K, CacheValue<V>> entry : cacheMap.entrySet()) {
CacheValue<V> cacheValue = entry.getValue();
if (Objects.equals(cacheValue.value, value)) {
if (cacheValue.ttlTime > now) {
return true;
}
remove(entry.getKey());
return false;
}
}
return false;
}
/**
* 从缓存中获取有效的值
*/
@Override
public V get(Object key) {
CacheValue<V> cacheValue = cacheMap.get(key);
if (cacheValue != null) {
cacheList.remove(key);
if (cacheValue.ttlTime > System.currentTimeMillis()) {
cacheList.addLast((K) key);
return cacheValue.value;
}
cacheMap.remove(key);
}
return null;
}
/**
* 从缓存中获取有效的值, 如果不存在, 则缓存默认值, 并返回
*
* @param key 键
* @param defaultV 默认值
*/
public V get(K key, V defaultV) {
return get(key, defaultV, defaultTtlTime);
}
/**
* 从缓存中获取有效的值, 如果不存在, 则缓存默认值, 并返回
*
* @param key 键
* @param defaultV 默认值
* @param ttlTime 缓存默认值的有效时间(毫秒)
*/
public V get(K key, V defaultV, long ttlTime) {
V v = get(key);
if (v != null) {
return v;
}
put(key, defaultV, ttlTime);
return defaultV;
}
/**
* 从缓存中获取有效的值, 如果不存在, 则缓存方法的返回值, 并返回
*
* @param key 键
* @param function 提供值的方法
*/
public V get(K key, Function<K, V> function) {
return get(key, function, defaultTtlTime);
}
/**
* 如果不存在, 则缓存方法的返回值, 并返回
*
* @param key 键
* @param function 提供值的方法
* @param ttlTime 缓存方法返回值的有效时间(毫秒)
*/
public V get(K key, Function<K, V> function, long ttlTime) {
V v = get(key);
if (v != null) {
return v;
}
V value = function.apply(key);
put(key, value, ttlTime);
return value;
}
/**
* 缓存数据
*/
@Override
public V put(K key, V value) {
return put(key, value, defaultTtlTime);
}
/**
* 缓存数据
*
* @param key 键
* @param value 值
* @param ttlTime 过期时间(毫秒), -1 表示不过期.
*/
public V put(K key, V value, long ttlTime) {
if (ttlTime == 0) {
return null;
} else if (ttlTime < 0) {
ttlTime = -1L;
} else {
ttlTime = System.currentTimeMillis() + ttlTime;
}
if (cacheMap.containsKey(key)) {
remove(key);
} else if (cacheList.size() >= maxCapacity) {
cleanUpExpired();
if (cacheList.size() >= maxCapacity) {
synchronized (this) {
cacheMap.remove(cacheList.removeFirst());
}
}
}
CacheValue<V> cacheValue = new CacheValue<>(value, ttlTime);
synchronized (this) {
cacheList.addLast(key);
cacheMap.put(key, cacheValue);
}
return value;
}
/**
* 设置 key 缓存时间
*
* @param key 键
* @param ttlTime 过期时间(毫秒), -1 表示不过期.
*/
public boolean expire(K key, long ttlTime) {
long now = System.currentTimeMillis();
if (ttlTime < 0) {
ttlTime = -1L;
} else {
ttlTime = now + ttlTime;
}
CacheValue<V> cacheValue = cacheMap.get(key);
if (cacheValue != null) {
if (cacheValue.ttlTime == -1 || cacheValue.ttlTime > now) {
cacheValue.ttlTime = ttlTime;
return true;
}
remove(key);
return false;
}
return false;
}
/**
* 删除缓存值
*/
@Override
public synchronized V remove(Object key) {
CacheValue<V> remove = cacheMap.remove(key);
if (remove != null) {
cacheList.remove(key);
return remove.value;
}
return null;
}
/**
* 全部增加进缓存
*/
@Override
public synchronized void putAll(Map<? extends K, ? extends V> m) {
if (!m.isEmpty()) {
for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
this.put(entry.getKey(), entry.getValue());
}
}
}
/**
* 清空缓存
*/
@Override
public synchronized void clear() {
cacheMap.clear();
cacheList.clear();
}
/**
* 有效值的键
*/
@Override
public Set<K> keySet() {
Set<K> set = new HashSet<>();
long now = System.currentTimeMillis();
if (!cacheMap.isEmpty()) {
for (Entry<K, CacheValue<V>> entry : cacheMap.entrySet()) {
if (entry.getValue().ttlTime > now) {
set.add(entry.getKey());
} else {
remove(entry.getKey());
}
}
}
return set;
}
/**
* 有效值
*/
@Override
public Collection<V> values() {
ArrayList<V> vs = new ArrayList<>();
long now = System.currentTimeMillis();
if (!cacheMap.isEmpty()) {
for (Entry<K, CacheValue<V>> entry : cacheMap.entrySet()) {
CacheValue<V> value = entry.getValue();
if (value.ttlTime > now) {
vs.add(value.value);
} else {
remove(entry.getKey());
}
}
}
return vs;
}
/**
* 有效值的键值对
*/
@Override
public Set<Entry<K, V>> entrySet() {
Set<Entry<K, V>> set = new LinkedHashSet<>();
long now = System.currentTimeMillis();
for (Entry<K, CacheValue<V>> entry : cacheMap.entrySet()) {
if (entry.getValue().ttlTime > now) {
Entry<K, V> cacheEntry = new CacheEntry<>(entry.getKey(), entry.getValue().value);
set.add(cacheEntry);
} else {
remove(entry.getKey());
}
}
return set;
}
/**
* 定期全面清理过期数据.
*/
private void cleanUpExpired() {
long last = System.currentTimeMillis();
while (true) {
long now = System.currentTimeMillis();
while (now - ONE_MINUTE < last) {
Thread.yield();
now = System.currentTimeMillis();
}
for (Entry<K, CacheValue<V>> entry : cacheMap.entrySet()) {
CacheValue<V> value = entry.getValue();
if (value.ttlTime <= now) {
synchronized (this) {
cacheMap.remove(entry.getKey());
cacheList.remove(entry.getKey());
}
}
}
last = now;
}
}
@AllArgsConstructor
static class CacheValue<V> {
/**
* 缓存对象
*/
private V value;
/**
* 缓存过期时间
*/
private long ttlTime;
}
@AllArgsConstructor
static class CacheEntry<K, V> implements Map.Entry<K, V> {
private K k;
private V v;
@Override
public K getKey() {
return k;
}
@Override
public V getValue() {
return v;
}
@Override
public V setValue(V value) {
return this.v = value;
}
}
}
主要功能解释
-
获取缓存数据
java/** * 从缓存中获取指定键对应的值,同时进行缓存项的有效性检查和列表维护。 * <p> * 方法执行流程如下: * 1. 尝试从缓存映射(cacheMap)中根据键获取对应的CacheValue对象。 * 2. 如果CacheValue不为null,说明存在该缓存项: * a. 从使用频率列表(cacheList)中移除该键,模拟最近最少使用(LRU)策略的更新。 * b. 检查CacheValue的过期时间(ttlTime)是否大于当前时间,确认缓存项是否已过期。 * - 如果未过期,将该键重新添加到cacheList的末尾,表示最近被访问,然后返回缓存的值。 * - 如果已过期,从缓存映射中移除该过期项。 * 3. 如果CacheValue为null,说明请求的键在缓存中不存在,直接返回null。 * * @param key 缓存项的键。 * @return 缓存中的值,如果键不存在或已过期,则返回null。 */ @Override public V get(Object key) { // 获取缓存中的值及过期信息 CacheValue<V> cacheValue = cacheMap.get(key); // 检查缓存项是否存在 if (cacheValue != null) { // 移除并重新排序,模拟LRU更新 cacheList.remove(key); // 检查是否过期 if (cacheValue.ttlTime > System.currentTimeMillis()) { // 未过期,更新使用频率列表并返回值 cacheList.addLast((K) key); return cacheValue.value; } // 已过期,清理过期缓存 else { cacheMap.remove(key); } } // 缓存项不存在或已过期,返回null return null; }
-
录入缓存数据
java/** * 向缓存中添加或更新一个键值对,并设定过期时间。 * <p> * 此方法处理了以下逻辑: * 1. 验证过期时间(ttlTime),确保其为有效值。若ttlTime为0,则不执行任何操作并直接返回null。 * 若ttlTime小于0,则将其视为不过期(ttlTime设为-1)。否则,将其转换为绝对过期时间(当前时间加上ttlTime)。 * 2. 检查键是否已存在于缓存中。如果存在,则先移除旧的缓存项。 * 3. 检查缓存是否已达到最大容量。如果是,先调用cleanUpExpired()方法尝试清理过期项。 * 如果清理后仍然满载,则同步移除最不常用的项(cacheList的第一个元素)以腾出空间。 * 4. 使用提供的键、值和过期时间创建一个新的CacheValue对象。 * 5. 在同步块中执行实际的添加操作,以确保线程安全: * a. 将键添加到使用频率列表(cacheList)的末尾,表示此键刚被使用。 * b. 将新的CacheValue对象放入缓存映射(cacheMap)中。 * 6. 最后返回缓存的值。 * * @param key 缓存项的键。 * @param value 缓存项的值。 * @param ttlTime 缓存项的过期时间(毫秒),-1 表示不过期,0 表示不执行插入操作。 * @return 被缓存的值,如果ttlTime为0,则返回null。 */ public V put(K key, V value, long ttlTime) { // 校正过期时间ttlTime,确保其为有效值 if (ttlTime == 0) { return null; } else if (ttlTime < 0) { ttlTime = -1L; } else { ttlTime = System.currentTimeMillis() + ttlTime; } // 如果键已存在,先移除旧的缓存项 if (cacheMap.containsKey(key)) { remove(key); } // 检查并维护缓存的最大容量 if (cacheList.size() >= maxCapacity) { cleanUpExpired(); // 尝试清理过期项 if (cacheList.size() >= maxCapacity) { synchronized (this) { // 如果清理后仍满,移除最不常用项 cacheMap.remove(cacheList.removeFirst()); } } } // 准备新的CacheValue对象 CacheValue<V> cacheValue = new CacheValue<>(value, ttlTime); // 同步添加新缓存项到map和list synchronized (this) { cacheList.addLast(key); cacheMap.put(key, cacheValue); } // 返回缓存的值 return value; }
使用实例
位置
心理健康产品模块的心理健康控制器中
-
引入本地缓存
-
接口中使用(查询心理健康产品列表接口:/list)
-
第一次调用本接口可以看到并没有命中缓存,查询数据库后存入本地缓存(过期时间设置为10000毫秒),并进行数据返回
成功返回数据
-
第二次调用本接口(成功命中缓存,直接进行数据返回!)
参考文献
- java缓存技术实例_springboot的缓存技术的实现
- 干货|java缓存技术详解
- JAVA几种缓存技术介绍说明
- 【java缓存、redis缓存、guava缓存】java中实现缓存的几种方式
- Java 缓存
- Java本地高性能缓存的几种实现方式
- Ehcache使用教程
- 一文通透讲解Redis高级特性,多线程/持久化/淘汰机制等统统搞定
- Redis八大特性
- Spring cache 缓存介绍
- Spring Cache的介绍以及使用方法、常用注解
- Spring Cache的基本使用与分析
- 「Mybatis系列」Mybatis缓存
- MyBatis缓存功能原理及实例解析
- Mybatis缓存机制详解
- 来,5W1H分析法帮你系统掌握缓存!
- 缓存是什么 什么是缓存
- 什么是缓存
- Ehcache注解核心逻辑源码学习
- Redis详细讲解(redis原理,redis安装,redis配置,redis使用,redis命令)
- 深度剖析Redis九种数据结构实现原理,建议收藏
- Redis 系列(九):Redis 集合底层实现原理
- Spring cache原理详解,分享给大家
- 【深入浅出Spring原理及实战】「缓存Cache开发系列」带你深入分析Spring所提供的缓存Cache抽象详解的核心原理探索
- MyBatis的二级缓存原理是什么?
- 【Mybatis源码分析】SqlSessionFactory二级缓存原理分析
- Mybatis源码剖析(二级缓存原理)