《Apache Shiro 源码解析》- 8. 缓存
8.缓存
本章将深入探讨 Shiro 的缓存架构,并对核心组件的源代码进行解析。
8.1 Shiro 为什么引入缓存机制
随着用户规模的不断扩大,认证、授权和加密等模块的调用次数会迅速增加。例如,当每秒有 100 万用户尝试登录系统时,认证模块每秒会被调用 100 万次。此时, CPU 和 Memory 都会飙升,性能问题将不可避免地浮现出来。
那么,如何在架构层面解决这些可能出现的性能瓶颈呢?最常见的解决方案就是引入缓存机制。有很多数据实际上并不需要在每次请求中都重新计算,我们可以将计算结果缓存起来,至少在一个特定的时间段以内,都可以直接从缓存中捞出数据,从而显著降低系统资源的消耗。
提升性能正是 Shiro 框架引入缓存机制的一个重要原因。
在 Shiro 中,缓存主要用于以下 3 个方面:
- 认证缓存:存储用户的认证信息,避免每次请求都需要重新认证。
- 授权缓存:存储用户的角色和权限信息,避免每次访问资源都去查询数据库获取权限。
- Session 缓存: 用来缓存会话信息。
注意:默认情况下,Shiro 并不会启用任何缓存,开发者需要在 ShiroConfig.java
中显式配置缓存管理器,指定 Shiro 应该使用哪种缓存组件。
@Configuration public class ShiroConfig { //... @Bean public EhCacheManager ehCacheManager(){ EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml"); return cacheManager; } //... }
8.2 Shiro 的缓存架构
8.2.1 核心组件
Shiro 的缓存架构由 3 个核心接口组成: CachingRealm
、CacheManager
、和Cache
,这些类型之间的依赖关系如下图所示:
这些类型的名称都带有 Cache 或者 Caching 前缀:
- CachingRealm:Realm 的实现类,它会持有一个 CacheManager 的实例。
- CacheManager:缓存管理器,负责管理缓存组件的生命周期,它会持有具体缓存组件的实例。
- Cache:缓存组件本身需要实现的接口。
8.2.2 源码分析
我们先分析整体的运行机制,然后再逐步解析核心组件的源代码。
8.2.2.1 整体运行机制
如上图所示,CachingRealm
中持有了一个 CacheManager
类型的实例,而 CachingSecurityManager
类中也持有了一个 CacheManager
类型的实例。那么,这两个 CacheManager
类型的实例之间是什么关系呢?是同一个实例吗?
我们来逐步分析源代码,我们从入口类 ShiroConfig.java 开始,在创建具体的 SecurityManager
实例时,开发者可以指定具体使用哪一种 CacheManager
,示例代码如下:
@Bean public EhCacheManager ehCacheManager(){ EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml"); return cacheManager; } @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(nicefishRbacRealm()); securityManager.setRememberMeManager(rememberMeManager()); securityManager.setSessionManager(sessionManager()); **securityManager.setCacheManager(ehCacheManager());** return securityManager; } //...
从以上代码可以看到,开发者只需要调用 securityManager.setCacheManager 方法设置缓存管理器就可以了,并不需要手动调用 CachingRealm
类型上定义的 setCacheManager 方法。那么,CachingRealm
类型上定义的 setCacheManager 方法是何时被自动调用的呢?我们再次回顾以下 SecurityManager
相关的继承结构:
在 RealmSecurityManager
这一层,我们可以看到如下代码:
public void setRealms(Collection<Realm> realms) { if (realms == null) { throw new IllegalArgumentException("Realms collection argument cannot be null."); } if (realms.isEmpty()) { throw new IllegalArgumentException("Realms collection argument cannot be empty."); } this.realms = realms; afterRealmsSet(); } protected void afterRealmsSet() { applyCacheManagerToRealms(); applyEventBusToRealms(); } protected void applyCacheManagerToRealms() { CacheManager cacheManager = getCacheManager(); Collection<Realm> realms = getRealms(); if (cacheManager != null && realms != null && !realms.isEmpty()) { for (Realm realm : realms) { if (realm instanceof CacheManagerAware) { ((CacheManagerAware) realm).setCacheManager(cacheManager); } } } }
关键的方法调用轨迹是: setRealms->afterRealmsSet->applyCacheManagerToRealms 。在 applyCacheManagerToRealms 中,如果 Shiro 发现某个 Realm 的实例实现了 CacheManagerAware 接口,就会自动把 cacheManager 实例设置给它。
// CachingRealm 的构造方法 public abstract class CachingRealm implements Realm, Nameable, **CacheManagerAware** , LogoutAware
如上所示,由于 CachingRealm 实现了 CacheManagerAware 接口,所以在运行时 CachingRealm 和 RealmSecurityManager 上的 cacheManager 是同一个实例。这就意味着,开发者在配置缓存管理器时,应该调用 securityManager 对象上的 setCacheManager 方法,而不是调用 Realm 实例上的同名方法,否则在运行时 cacheManager 实例会被覆盖。
8.2.2.2 CacheManager 源码分析
CacheManager
是 Shiro 缓存系统的核心接口,它负责管理缓存组件的生命周期,以下是 CacheManager
相关的继承结构图:
Destroyable
接口 :定义了destroy()
方法,用于在缓存管理器销毁时清理资源,确保缓存的数据和资源得以正确释放。CacheManager
接口 :提供获取缓存的核心方法getCache(String name)
,是所有缓存管理器的顶层接口。通过实现CacheManager
接口,开发者可以自定义缓存管理器以适应不同的缓存需求。AbstractCacheManager
:作为抽象基类,它为CacheManager
提供了createCache(String name)
的基础实现,并实现了destroy()
方法。这意味着它具备缓存管理器的基础功能,可以销毁缓存,同时允许子类根据需要创建特定类型的缓存。MemoryConstrainedCacheManager
:Shiro 提供的轻量级缓存管理器,继承自AbstractCacheManager
。它将所有缓存数据保存在 JVM 内存中,适合小型应用或资源有限的环境。由于其缓存是基于内存的,一旦 JVM 重启,缓存数据将会丢失。这使得该缓存管理器在处理敏感数据时的持久化能力较差,因此主要适用于对数据持久性要求不高的场景。EhCacheManager
:此缓存管理器继承自AbstractCacheManager
,并实现了CacheManager
和Destroyable
接口。EhCacheManager
集成了开源的 EhCache 框架。EhCache 支持磁盘持久化、多级缓存(内存+磁盘缓存)、集群部署等功能,适用于中大型应用。通过cacheManagerConfigFile
配置文件,EhCacheManager 可以对缓存进行自定义配置,确保在高并发情况下提供更好的性能和可扩展性。
8.2.2.3 AbstractCacheManager 源码解析
AbstractCacheManager 是抽象基类,它实现了缓存管理的基本功能,包括:
- 缓存的惰性创建:只有在第一次访问某个缓存时,才会创建该缓存。
- 线程安全管理:通过使用 ConcurrentHashMap 确保缓存管理器在并发环境中的安全性。
- 缓存销毁机制:提供 destroy() 方法,用于清理资源并销毁所有缓存。
AbstractCacheManager 的源代码非常简单,去掉注释之后只有几十行。其中,createCache(String name) 是一个关键的抽象方法,具体的缓存创建逻辑由子类实现,例如 MemoryConstrainedCacheManager 和 EhCacheManager 会分别实现该方法,以创建特定类型的缓存实例。
8.2.2.4 MemoryConstrainedCacheManager 源码解析
MemoryConstrainedCacheManager
是基于内存的缓存管理器,它的源码非常简单,源代码全文引用如下:
package org.apache.shiro.cache; import org.apache.shiro.util.SoftHashMap; public class MemoryConstrainedCacheManager extends AbstractCacheManager { @Override protected Cache createCache(String name) { //注意,这里创建的缓存实例类型是 MapCache return new MapCache<Object, Object>(name, new SoftHashMap<Object, Object>()); } }
从代码中我们可以看到,这个管理器持有了一个 MapCache 的实例,具体的数据就存储在这个实例里面。 MapCache 是 Shiro 自己实现的一个简单缓存,基于 JDK 内置的 Map 实现,我们在 8.2.3 中分析它的源代码。
MemoryConstrained 这个单词的字面意思是"内存受限" ,所以 MemoryConstrainedCacheManager
这个类名已经暗示了它的适用场景:
- 内存受限的环境:适合内存有限的应用场景,比如嵌入式系统、移动设备或需要在服务器端进行精确内存控制的应用。
- 简单缓存管理:不需要外部依赖(如 Redis)的简单缓存场景,能够快速使用内存缓存。
8.2.2.5 EhCacheManager 源码解析
EhCacheManager 的实现同样非常简单,就如同它的名字一样,主要负责创建并管理 EhCache 组件的实例,与此相关的代码在 EhCacheManager.ensureCacheManager 方法中:
private net.sf.ehcache.CacheManager ensureCacheManager() { try { if (this.manager == null) { if (log.isDebugEnabled()) { log.debug("cacheManager property not set. Constructing CacheManager instance... "); } //using the CacheManager constructor, the resulting instance is _not_ a VM singleton //(as would be the case by calling CacheManager.getInstance(). We do not use the getInstance here //because we need to know if we need to destroy the CacheManager instance - using the static call, //we don't know which component is responsible for shutting it down. By using a single EhCacheManager, //it will always know to shut down the instance if it was responsible for creating it. //注意这里, new 出了 Ehcache 的实例。 this.manager = new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream()); if (log.isTraceEnabled()) { log.trace("instantiated Ehcache CacheManager instance."); } cacheManagerImplicitlyCreated = true; if (log.isDebugEnabled()) { log.debug("implicit cacheManager created successfully."); } } return this.manager; } catch (Exception e) { throw new CacheException(e); } }
Shiro 提供了一个独立的 jar 包用来封装对 Ehcache 的支持,包名为 shiro-ehcache.jar ,这个包非常小,里面只有 2 个类和一个默认的 ehcache.xml 配置文件:
EhCache 是一个轻量级、快速且功能强大的开源 Java 缓存框架,广泛应用于提高 Java 应用程序性能。它支持内存和磁盘存储、多种缓存失效策略和分布式缓存,非常适合需要频繁访问数据的高性能场景。 EhCache 的主要特性如下:
- 多级缓存:支持在内存和磁盘中存储缓存,内存满时可以自动将数据写入磁盘。
- 缓存策略:支持多种缓存失效策略,如最少使用 (LFU)、最近最少使用 (LRU)、FIFO 等。
- 分布式缓存:可以与 Terracotta 等分布式缓存框架集成,实现多节点共享缓存数据。
- 数据持久性:可以选择在应用重启后缓存数据是否保留。
- 可配置性:通过 XML 或 Java API 配置缓存的大小、存储方式、失效时间等。
EhCache 的官方网站是 https://www.ehcache.org/。
在该网站上,你可以找到有关 EhCache 的最新版本、文档、配置指南、使用示例等资源。
8.2.2.6 对接 Redis 缓存
当前,在分布式系统中,架构师一般会选择 Redis 作为缓存组件,但是,Shiro 并没有直接提供一个开箱即用的 RedisCacheManager (原因简单,因为当年 Redis 还没有出现。)。开发者可以自己实现 CacheManager 接口,也可以选择开源的实现,例如 shiro-redis ,它的主页在:https://github.com/alexxiyang/shiro-redis 。
8.2.3 Cache 源码分析
接下来我们分析缓存本身的实现,在 Shiro 中,Cache
接口相关的类继承结构如下图所示:
MapCache
是 Shiro 自己提供的一个非常简单的缓存实现类,它的内部实际上使用了一个 Map<K,V>
结构来存储数据,以下是MapCache
的关键源代码:
public class MapCache<K, V> implements Cache<K, V> { private final Map<K, V> map; //缓存的名称 private final String name; //构造方法要求外部传递一个具体的 Map 实例进来, Shiro 默认使用自己实现的 SoftHashMap 类。 public MapCache(String name, Map<K, V> backingMap) { if (name == null) { throw new IllegalArgumentException("Cache name cannot be null."); } if (backingMap == null) { throw new IllegalArgumentException("Backing map cannot be null."); } this.name = name; this.map = backingMap; } //... }
Shiro 自己实现了一个 SoftHashMap
来承担存储任务,这个类位于 shiro-core-xxx.jar 包中。SoftHashMap
基于软引用的哈希映射类实现,可以在内存不足时能够自动回收不再使用的缓存内容。以下是其核心功能:
- 软引用存储 :使用
SoftReference
来存储值对象,这样在内存压力大的情况下,JVM 可以自动回收这些值,避免内存溢出。 - 强引用管理:维护一个强引用队列,允许开发者控制保留的强引用数量,以平衡内存使用和缓存命中率。
- 自动清理:在访问缓存时,会自动处理和清理已被回收的软引用,保持映射的有效性。
- 线程安全 :使用
ConcurrentHashMap
和ReentrantLock
确保在多线程环境下的安全性和一致性。 - 接口实现 :实现了
Map
接口,提供标准的 Map 操作(如put
、get
、remove
等)并支持批量操作。
也就是说,如果开发者指定 Shiro 使用 MemoryConstrainedCacheManager
作为缓存管理器,那么在运行时,最底层承担存储任务的是 SoftHashMap
类的实例。
8.2.4 CachingRealm 源码分析
由于 Realm
相关的继承结构比较深,我们需要再次回顾一下结构图:
CachingRealm
是 Realm
的实现类,它的类名带有 Caching 前缀,很明显它最大的特点就是带有缓存功能。由于 CachingRealm
在整个继承结构中位置非常高,所以在 Shiro 中,所有 Realm 都具备缓存功能,除非开发者自己编写一个全新的实现类直接实现最顶层的 Realm
接口。但是这种情况非常少见,因为 Shiro 在每一层 Realm 上都已经实现了很多功能,如果自己从头实现 Realm
接口,需要编写的代码太多了。
如上图所示,CachingRealm
的功能非常简单,实际上它自己几乎没有实现任何功能,把所有具体工作都交给内部的 cacheManager 对象去处理了:
public abstract class CachingRealm implements Realm, Nameable, CacheManagerAware, LogoutAware { //... private String name; private boolean cachingEnabled; private CacheManager cacheManager; //实际上是 cacheManager 在做事 //... }
8.3 Session 缓存
在 Shiro 中,Session
默认并不会自动走缓存,但 Shiro 设计了缓存会话的机制。
8.3.1 启用 Session 缓存
Shiro 的 Session
默认存储在内存中,如果没有明确配置缓存,Shiro 不会自动缓存 Session
数据。
如果希望将会话信息缓存起来,可以配只 CacheManager
配置项,通常会使用开源的 EhCacheManager
或者RedisCacheManager
,以下是使用EhCacheManager
作为 Session 缓存的关键代码(已省略无关代码):
EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml"); CachingSessionDAO sessionDAO = new EnterpriseCacheSessionDAO(); sessionDAO.setCacheManager(cacheManager); // 配置 cacheManager DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(sessionDAO); DefaultSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setSessionManager(sessionManager); SecurityUtils.setSecurityManager(securityManager);
8.3.2 CachingSessionDAO 源码分析
如上图所示, Shiro 内置的抽象类 CachingSessionDAO
支持缓存机制,EnterpriseCacheSessionDAO
是 CachingSessionDAO
的子类,在 CachingSessionDAO
中,最关键的 4 个方法代码如下:
public Serializable create(Session session) { Serializable sessionId = super.create(session); cache(session, sessionId); return sessionId; } public Session readSession(Serializable sessionId) throws UnknownSessionException { Session s = getCachedSession(sessionId); if (s == null) { s = super.readSession(sessionId); } return s; } public void update(Session session) throws UnknownSessionException { doUpdate(session); if (session instanceof ValidatingSession) { if (((ValidatingSession) session).isValid()) { cache(session, session.getId()); } else { uncache(session); } } else { cache(session, session.getId()); } } public void delete(Session session) { uncache(session); doDelete(session); }
- create: 先写持久层,然后再写缓存。
- readSession: 先尝试从缓存中读取 Session ,如果结果为 null ,调用父层的 readSession 去访问持久层。
- update: 先更新持久层,然后更新缓存。
- delete: 先删缓存中的数据,然后再删持久层数据。
从以上代码我们可以看到,CachingSessionDAO
对缓存的读写策略都非常简单,比如 readSession 方法,采用的是 Read-Through(读透)策略:如果没有能够从缓存中读取到数据,直接访问持久层,很容易形成系统瓶颈。
8.3.3 注意
在启用了 Session 缓存之后,系统的复杂度会显著增加。这是因为缓存中的数据与数据库中的数据可能会存在不一致的情况,容易导致潜在的逻辑错误。因此,开发者在设计系统时,需要谨慎处理 Session 缓存,以确保数据的一致性。
Shiro 内置的 CachingSessionDAO
提供了一种简单的实现方式,方便开发者快速集成缓存功能。然而, Shiro 毕竟是一个安全框架,并不是专业的缓存框架,开发者在面对更复杂的业务需求时,可能需要设计自己的缓存 DAO。这种自定义的缓存 DAO 可以提供对 Session 的缓存进行更细粒度的控制,从而优化系统性能,减少对数据库的压力,并提高响应速度。
在设计自己的缓存 DAO 时,开发者可以考虑以下几个方面:
- 缓存策略选择:根据业务场景选择合适的缓存策略,例如读透、写穿或写后失效等,以确保在性能和一致性之间找到平衡。
- 过期与失效管理:设置合理的缓存过期时间,以避免缓存中存储过期数据。同时,设计手动失效机制,以确保重要数据的实时更新。
- 监控与调优:通过监控缓存的使用情况,分析命中率和访问模式,从而不断调整和优化缓存策略,确保系统的高效运行。
在分布式系统中,推荐使用 Redis 作为 Session 的缓存解决方案。Redis 具有高性能、支持持久化和跨模块、跨系统共享数据的能力,能够有效地管理会话数据。在这种架构下,各个模块和系统之间可以复用会话信息,提升用户体验并简化系统管理。
综上所述,虽然 Session 缓存可以带来性能提升,但也引入了额外的复杂性。开发者应仔细权衡利弊,并在必要时实施更灵活和高效的缓存管理策略,以实现更高效、可靠的系统架构。
8.4 本章小结
为了提升系统的性能,Shiro 内置了对缓存的支持。特别是在频繁的权限验证过程中,缓存的引入能极大减少系统的负载。本章详细解析了 Shiro 的缓存架构,并解析了如何与外部缓存组件进行对接。
资源链接
- Apache Shiro 在 github 上的官方仓库:https://github.com/apache/shiro
- Apache Shiro 官方网站:https://shiro.apache.org/
- 本书实例项目:https://gitee.com/mumu-osc/nicefish-spring-boot
- 本书文字稿:https://gitee.com/mumu-osc/apache-shiro-source-code-explaination
版权声明
本书基于 CC BY-NC-ND 4.0 许可协议发布,自由转载-非商用-非衍生-保持署名。
版权归大漠穷秋所有 © 2024 ,侵权必究。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Vim 项目现状
Vim 创始人及终身仁慈独裁者(BDFL)Bram Moolenaar 于 2023 年的离世让社区感到震惊,同时也引发了对项目未来的担忧。 在 2024 年 11 月举行的 VimConf 大会上,现任 Vim 维护者 Christian Brabandt 发表主题演讲“Vim 项目的新生”(the new Vim project"),介绍了社区如何重组以继续维护 Vim,以及未来的发展方向。 “后 Bram 时代”的 Vim Brabandt 首先回顾了他与 Vim 的渊源:他自 2006 年起参与 Vim 项目,并表示他的首次提交是在 7.0/7.1 版本时期(大约在 2006 年)。起初,他主要贡献小补丁和修复,后来逐渐贡献了更大的功能,比如 gn 和 gN 命令(结合搜索和可视模式选择)、使用 libsodium 改进的加密支持,维护 Vim 的 AppImage 等。他提到,由于个人和工作的原因,他在 2022 年左右减少了对项目的参与。 这一情况在 2023 年 8 月发生了变化,因为 Moolenaar 去世了。Moolenaar 维护 Vim 已超过 30 年;尽管在...
- 下一篇
“苹果 AI”有望在 2025 年亮相中国,已成立新公司
苹果智能(Apple Intelligence)有望在 2025 年正式亮相中国市场。 据企查查官方消息,1月10日,苹果技术开发(上海)有限公司成立,法定代表人为Tejas Kirit Gala,注册资本3500万美元。 公开数据显示,该公司行业属于软件开发,主要经营范围涵盖软件开发、大数据服务、数据处理服务以及存储支持服务等。股权穿透显示,该公司由APPLE SOUTH ASIA PTE. LTD.全资持股。 苹果CEO库克在2024年三次访华期间,曾提到关于中国市场推出AI手机的计划,并强调了公司正在努力推进这一计划。 因此有理由推测,苹果公司通过这家新公司,在中国加速推进Apple Intelligence服务落地。 研究机构Canalys的分析师认为,苹果在中国提供的Apple Intelligence服务不仅可能包含自研大模型,还将整合中国本地科技公司的技术。 相关阅读:苹果与腾讯、字节接洽,考虑将二者 AI 模型整合到国行 iPhone
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Windows10,CentOS7,CentOS8安装Nodejs环境
- 设置Eclipse缩进为4个空格,增强代码规范
- CentOS7设置SWAP分区,小内存服务器的救世主
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Hadoop3单机部署,实现最简伪集群