8.缓存 - 图1

8.缓存

本章将深入探讨 Shiro 的缓存架构,并对核心组件的源代码进行解析。

8.1 Shiro 为什么引入缓存机制

随着用户规模的不断扩大,认证、授权和加密等模块的调用次数会迅速增加。例如,当每秒有 100 万用户尝试登录系统时,认证模块每秒会被调用 100 万次。此时, CPU 和 Memory 都会飙升,性能问题将不可避免地浮现出来。

那么,如何在架构层面解决这些可能出现的性能瓶颈呢?最常见的解决方案就是引入缓存机制。有很多数据实际上并不需要在每次请求中都重新计算,我们可以将计算结果缓存起来,至少在一个特定的时间段以内,都可以直接从缓存中捞出数据,从而显著降低系统资源的消耗。

提升性能正是 Shiro 框架引入缓存机制的一个重要原因。

在 Shiro 中,缓存主要用于以下 3 个方面:

  • 认证缓存:存储用户的认证信息,避免每次请求都需要重新认证。
  • 授权缓存:存储用户的角色和权限信息,避免每次访问资源都去查询数据库获取权限。
  • Session 缓存: 用来缓存会话信息。

注意:默认情况下,Shiro 并不会启用任何缓存,开发者需要在 ShiroConfig.java 中显式配置缓存管理器,指定 Shiro 应该使用哪种缓存组件。

  1. @Configuration
  2. public class ShiroConfig {
  3. //...
  4. @Bean
  5. public EhCacheManager ehCacheManager(){
  6. EhCacheManager cacheManager = new EhCacheManager();
  7. cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
  8. return cacheManager;
  9. }
  10. //...
  11. }

8.2 Shiro 的缓存架构

8.2.1 核心组件

Shiro 的缓存架构由 3 个核心接口组成: CachingRealmCacheManager、和Cache ,这些类型之间的依赖关系如下图所示:

8.缓存 - 图2

这些类型的名称都带有 Cache 或者 Caching 前缀:

  • CachingRealm:Realm 的实现类,它会持有一个 CacheManager 的实例。
  • CacheManager:缓存管理器,负责管理缓存组件的生命周期,它会持有具体缓存组件的实例。
  • Cache:缓存组件本身需要实现的接口。

8.2.2 源码分析

我们先分析整体的运行机制,然后再逐步解析核心组件的源代码。

8.2.2.1 整体运行机制

8.缓存 - 图3

如上图所示,CachingRealm 中持有了一个 CacheManager 类型的实例,而 CachingSecurityManager 类中也持有了一个 CacheManager 类型的实例。那么,这两个 CacheManager 类型的实例之间是什么关系呢?是同一个实例吗?

我们来逐步分析源代码,我们从入口类 ShiroConfig.java 开始,在创建具体的 SecurityManager 实例时,开发者可以指定具体使用哪一种 CacheManager ,示例代码如下:

  1. @Bean
  2. public EhCacheManager ehCacheManager(){
  3. EhCacheManager cacheManager = new EhCacheManager();
  4. cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
  5. return cacheManager;
  6. }
  7. @Bean
  8. public SecurityManager securityManager(){
  9. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  10. securityManager.setRealm(nicefishRbacRealm());
  11. securityManager.setRememberMeManager(rememberMeManager());
  12. securityManager.setSessionManager(sessionManager());
  13. **securityManager.setCacheManager(ehCacheManager());**
  14. return securityManager;
  15. }
  16. //...

从以上代码可以看到,开发者只需要调用 securityManager.setCacheManager 方法设置缓存管理器就可以了,并不需要手动调用 CachingRealm 类型上定义的 setCacheManager 方法。那么,CachingRealm 类型上定义的 setCacheManager 方法是何时被自动调用的呢?我们再次回顾以下 SecurityManager 相关的继承结构:

8.缓存 - 图4

RealmSecurityManager 这一层,我们可以看到如下代码:

  1. public void setRealms(Collection<Realm> realms) {
  2. if (realms == null) {
  3. throw new IllegalArgumentException("Realms collection argument cannot be null.");
  4. }
  5. if (realms.isEmpty()) {
  6. throw new IllegalArgumentException("Realms collection argument cannot be empty.");
  7. }
  8. this.realms = realms;
  9. afterRealmsSet();
  10. }
  11. protected void afterRealmsSet() {
  12. applyCacheManagerToRealms();
  13. applyEventBusToRealms();
  14. }
  15. protected void applyCacheManagerToRealms() {
  16. CacheManager cacheManager = getCacheManager();
  17. Collection<Realm> realms = getRealms();
  18. if (cacheManager != null && realms != null && !realms.isEmpty()) {
  19. for (Realm realm : realms) {
  20. if (realm instanceof CacheManagerAware) {
  21. ((CacheManagerAware) realm).setCacheManager(cacheManager);
  22. }
  23. }
  24. }
  25. }

关键的方法调用轨迹是: setRealms->afterRealmsSet->applyCacheManagerToRealms 。在 applyCacheManagerToRealms 中,如果 Shiro 发现某个 Realm 的实例实现了 CacheManagerAware 接口,就会自动把 cacheManager 实例设置给它。

  1. // CachingRealm 的构造方法
  2. 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 相关的继承结构图:

8.缓存 - 图5

  • 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 是基于内存的缓存管理器,它的源码非常简单,源代码全文引用如下:

  1. package org.apache.shiro.cache;
  2. import org.apache.shiro.util.SoftHashMap;
  3. public class MemoryConstrainedCacheManager extends AbstractCacheManager {
  4. @Override
  5. protected Cache createCache(String name) {
  6. //注意,这里创建的缓存实例类型是 MapCache
  7. return new MapCache<Object, Object>(name, new SoftHashMap<Object, Object>());
  8. }
  9. }

从代码中我们可以看到,这个管理器持有了一个 MapCache 的实例,具体的数据就存储在这个实例里面。 MapCache 是 Shiro 自己实现的一个简单缓存,基于 JDK 内置的 Map 实现,我们在 8.2.3 中分析它的源代码。

MemoryConstrained 这个单词的字面意思是“内存受限” ,所以 MemoryConstrainedCacheManager 这个类名已经暗示了它的适用场景:

  • 内存受限的环境:适合内存有限的应用场景,比如嵌入式系统、移动设备或需要在服务器端进行精确内存控制的应用。
  • 简单缓存管理:不需要外部依赖(如 Redis)的简单缓存场景,能够快速使用内存缓存。

8.2.2.5 EhCacheManager 源码解析

EhCacheManager 的实现同样非常简单,就如同它的名字一样,主要负责创建并管理 EhCache 组件的实例,与此相关的代码在 EhCacheManager.ensureCacheManager 方法中:

  1. private net.sf.ehcache.CacheManager ensureCacheManager() {
  2. try {
  3. if (this.manager == null) {
  4. if (log.isDebugEnabled()) {
  5. log.debug("cacheManager property not set. Constructing CacheManager instance... ");
  6. }
  7. //using the CacheManager constructor, the resulting instance is _not_ a VM singleton
  8. //(as would be the case by calling CacheManager.getInstance(). We do not use the getInstance here
  9. //because we need to know if we need to destroy the CacheManager instance - using the static call,
  10. //we don't know which component is responsible for shutting it down. By using a single EhCacheManager,
  11. //it will always know to shut down the instance if it was responsible for creating it.
  12. //注意这里, new 出了 Ehcache 的实例。
  13. this.manager = new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream());
  14. if (log.isTraceEnabled()) {
  15. log.trace("instantiated Ehcache CacheManager instance.");
  16. }
  17. cacheManagerImplicitlyCreated = true;
  18. if (log.isDebugEnabled()) {
  19. log.debug("implicit cacheManager created successfully.");
  20. }
  21. }
  22. return this.manager;
  23. } catch (Exception e) {
  24. throw new CacheException(e);
  25. }
  26. }

Shiro 提供了一个独立的 jar 包用来封装对 Ehcache 的支持,包名为 shiro-ehcache.jar ,这个包非常小,里面只有 2 个类和一个默认的 ehcache.xml 配置文件:

8.缓存 - 图6

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 接口相关的类继承结构如下图所示:

8.缓存 - 图7

MapCache是 Shiro 自己提供的一个非常简单的缓存实现类,它的内部实际上使用了一个 Map<K,V> 结构来存储数据,以下是MapCache的关键源代码:

  1. public class MapCache<K, V> implements Cache<K, V> {
  2. private final Map<K, V> map;
  3. //缓存的名称
  4. private final String name;
  5. //构造方法要求外部传递一个具体的 Map 实例进来, Shiro 默认使用自己实现的 SoftHashMap 类。
  6. public MapCache(String name, Map<K, V> backingMap) {
  7. if (name == null) {
  8. throw new IllegalArgumentException("Cache name cannot be null.");
  9. }
  10. if (backingMap == null) {
  11. throw new IllegalArgumentException("Backing map cannot be null.");
  12. }
  13. this.name = name;
  14. this.map = backingMap;
  15. }
  16. //...
  17. }

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 相关的继承结构比较深,我们需要再次回顾一下结构图:

8.缓存 - 图8

CachingRealmRealm 的实现类,它的类名带有 Caching 前缀,很明显它最大的特点就是带有缓存功能。由于 CachingRealm 在整个继承结构中位置非常高,所以在 Shiro 中,所有 Realm 都具备缓存功能,除非开发者自己编写一个全新的实现类直接实现最顶层的 Realm 接口。但是这种情况非常少见,因为 Shiro 在每一层 Realm 上都已经实现了很多功能,如果自己从头实现 Realm 接口,需要编写的代码太多了。

8.缓存 - 图9

如上图所示,CachingRealm的功能非常简单,实际上它自己几乎没有实现任何功能,把所有具体工作都交给内部的 cacheManager 对象去处理了:

  1. public abstract class CachingRealm implements Realm, Nameable, CacheManagerAware, LogoutAware {
  2. //...
  3. private String name;
  4. private boolean cachingEnabled;
  5. private CacheManager cacheManager; //实际上是 cacheManager 在做事
  6. //...
  7. }

8.3 Session 缓存

在 Shiro 中,Session 默认并不会自动走缓存,但 Shiro 设计了缓存会话的机制。

8.3.1 启用 Session 缓存

Shiro 的 Session 默认存储在内存中,如果没有明确配置缓存,Shiro 不会自动缓存 Session 数据。

如果希望将会话信息缓存起来,可以配只 CacheManager 配置项,通常会使用开源的 EhCacheManager或者RedisCacheManager ,以下是使用EhCacheManager作为 Session 缓存的关键代码(已省略无关代码):

  1. EhCacheManager cacheManager = new EhCacheManager();
  2. cacheManager.setCacheManagerConfigFile("classpath:shiro-ehcache.xml");
  3. CachingSessionDAO sessionDAO = new EnterpriseCacheSessionDAO();
  4. sessionDAO.setCacheManager(cacheManager); // 配置 cacheManager
  5. DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
  6. sessionManager.setSessionDAO(sessionDAO);
  7. DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
  8. securityManager.setSessionManager(sessionManager);
  9. SecurityUtils.setSecurityManager(securityManager);

8.3.2 CachingSessionDAO 源码分析

8.缓存 - 图10

如上图所示, Shiro 内置的抽象类 CachingSessionDAO 支持缓存机制,EnterpriseCacheSessionDAOCachingSessionDAO 的子类,在 CachingSessionDAO中,最关键的 4 个方法代码如下:

  1. public Serializable create(Session session) {
  2. Serializable sessionId = super.create(session);
  3. cache(session, sessionId);
  4. return sessionId;
  5. }
  6. public Session readSession(Serializable sessionId) throws UnknownSessionException {
  7. Session s = getCachedSession(sessionId);
  8. if (s == null) {
  9. s = super.readSession(sessionId);
  10. }
  11. return s;
  12. }
  13. public void update(Session session) throws UnknownSessionException {
  14. doUpdate(session);
  15. if (session instanceof ValidatingSession) {
  16. if (((ValidatingSession) session).isValid()) {
  17. cache(session, session.getId());
  18. } else {
  19. uncache(session);
  20. }
  21. } else {
  22. cache(session, session.getId());
  23. }
  24. }
  25. public void delete(Session session) {
  26. uncache(session);
  27. doDelete(session);
  28. }
  • 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 ,侵权必究。