您现在的位置是:首页 > 文章详情

《Apache Shiro 源码解析》- 8. 缓存

日期:2025-01-15点击:110

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 个核心接口组成: CachingRealmCacheManager、和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,并实现了 CacheManagerDestroyable 接口。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 基于软引用的哈希映射类实现,可以在内存不足时能够自动回收不再使用的缓存内容。以下是其核心功能:

  1. 软引用存储 :使用 SoftReference 来存储值对象,这样在内存压力大的情况下,JVM 可以自动回收这些值,避免内存溢出。
  2. 强引用管理:维护一个强引用队列,允许开发者控制保留的强引用数量,以平衡内存使用和缓存命中率。
  3. 自动清理:在访问缓存时,会自动处理和清理已被回收的软引用,保持映射的有效性。
  4. 线程安全 :使用 ConcurrentHashMapReentrantLock 确保在多线程环境下的安全性和一致性。
  5. 接口实现 :实现了 Map 接口,提供标准的 Map 操作(如 putgetremove 等)并支持批量操作。

也就是说,如果开发者指定 Shiro 使用 MemoryConstrainedCacheManager 作为缓存管理器,那么在运行时,最底层承担存储任务的是 SoftHashMap 类的实例。

8.2.4 CachingRealm 源码分析

由于 Realm 相关的继承结构比较深,我们需要再次回顾一下结构图:

CachingRealmRealm 的实现类,它的类名带有 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 支持缓存机制,EnterpriseCacheSessionDAOCachingSessionDAO 的子类,在 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 时,开发者可以考虑以下几个方面:

  1. 缓存策略选择:根据业务场景选择合适的缓存策略,例如读透、写穿或写后失效等,以确保在性能和一致性之间找到平衡。
  2. 过期与失效管理:设置合理的缓存过期时间,以避免缓存中存储过期数据。同时,设计手动失效机制,以确保重要数据的实时更新。
  3. 监控与调优:通过监控缓存的使用情况,分析命中率和访问模式,从而不断调整和优化缓存策略,确保系统的高效运行。

在分布式系统中,推荐使用 Redis 作为 Session 的缓存解决方案。Redis 具有高性能、支持持久化和跨模块、跨系统共享数据的能力,能够有效地管理会话数据。在这种架构下,各个模块和系统之间可以复用会话信息,提升用户体验并简化系统管理。

综上所述,虽然 Session 缓存可以带来性能提升,但也引入了额外的复杂性。开发者应仔细权衡利弊,并在必要时实施更灵活和高效的缓存管理策略,以实现更高效、可靠的系统架构。

8.4 本章小结

为了提升系统的性能,Shiro 内置了对缓存的支持。特别是在频繁的权限验证过程中,缓存的引入能极大减少系统的负载。本章详细解析了 Shiro 的缓存架构,并解析了如何与外部缓存组件进行对接。

资源链接

版权声明

本书基于 CC BY-NC-ND 4.0 许可协议发布,自由转载-非商用-非衍生-保持署名。

版权归大漠穷秋所有 © 2024 ,侵权必究。

原文链接:https://my.oschina.net/mumu/blog/16689206
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章