资源管理中的缓存策略
在软件开发中,缓存策略是优化资源使用和提高性能的重要手段。本文将介绍两种不同的缓存策略:享元模式(Flyweight Pattern)和外观模式(Façade Pattern),并详细阐述它们在实际应用中的使用方法和优势。
享元模式缓存策略
享元模式作为一种缓存机制,其有效性在很大程度上取决于所缓存数据的某些特性:
1.
大量对象的使用
:应用程序会使用大量的对象。
2.
高存储成本
:为多个用户复制大量对象会导致较高的存储(内存)成本。
3.
对象的不可变性或外部状态
:对象要么是不可变的,要么其状态可以外部化。
4.
共享对象的替代能力
:相对较少的共享对象可以替代多组对象。
5.
不依赖对象标识
:应用程序不依赖对象的标识,用户可能认为他们得到的是一个唯一的对象,但实际上是从缓存中获取的引用。
享元模式缓存的一个关键特性是对象中的状态信息。例如,在之前的示例中,就用户而言,产品对象是不可变的。如果允许用户对对象进行更改,那么这种缓存场景将无法工作,因为它依赖于存储在缓存中的对象是只读的。不过,使用享元设计模式也可以存储非不可变对象,但它们的部分状态信息必须位于对象外部。
在 eMotherEarth 应用中,特定商品的首选数量就是这种外部状态信息的一个很好的例子。这是特定于用户的信息,因此不应存储在缓存中。相反,这种偏好(以及其他类似信息)将存储在一个关联类中,该关联类与产品引用和产品对象之间的关系相关联。
需要注意的是,当缓存中的对象快速或意外地发生变化时,不建议使用享元设计模式。如果 eMotherEarth 应用中的产品每天更改多次,那么享元模式就不是一个合适的缓存策略。但就该应用的库存情况而言,这种情况似乎不太可能发生。享元模式最适用于大多数或所有用户共享一组不可变对象的情况,在这种情况下,内存节省效果显著,并且并发用户越多,效果越明显。
外观模式缓存策略
外观模式是另一种缓存方法,它通过为对象创建统一的接口来隐藏对象之间的复杂交互。其设计目的是通过为复杂的子系统提供友好的接口,使用户无需了解对象之间交互的复杂性,从而更轻松地使用该子系统。
与享元模式本身就是一种缓存策略不同,外观模式更有助于向日常开发者隐藏复杂缓存子系统的细节。它在应用程序的许多地方都非常有用。例如,当需要调用可重用库中的代码,而该库中的类之间存在复杂的交互时,可以使用外观模式将复杂的 API 封装成一个易于当前应用程序开发者使用的接口。
在 eMotherEarth 应用的一个迭代版本中,存在一个复杂的子系统,我们将使用外观模式对其进行处理。之前,我们为边界对象创建了一个 Commons 池,而使用 Commons GenericKeyedObjectPool 所需的接口非常详细,涉及多个相互交互的对象和工厂。应用程序的开发者无需了解这些对象之间的关系,甚至不需要知道他们使用的对象实际上来自缓存。在这种情况下,外观模式可以充当一种工厂对象,在不强迫开发者了解对象获取方式的情况下提供对象。
自动化外观创建
在这个示例中,我们使用的 eMotherEarth 版本来自之前的某个版本,该版本使用 Jakarta Commons 池工具来池化应用程序中的边界对象。为了将应用程序更新到最新的 Web API,我们对应用程序的一些方面进行了更改。
首先,在应用程序中创建池的方式发生了变化。在之前的示例中,我们引入了一个 GenericServlet 子类,并设置了一个启动配置参数以自动启动它。该启动 servlet 确保在应用程序启动时调用其 init() 方法。在使用较旧的 servlet 规范时,这是确保代码在应用程序启动时执行的最佳方式。但现在有了一种替代技术,我们将在这里实现它。我们不再创建一个初始化池的 servlet,而是创建一个 ServletContextListener(该接口自 servlet 2.3 API 起可用)。
ServletContextListener 允许开发者将事件与应用程序对象(即 ServletContext)关联起来。该监听器类是一个接口,包含两个方法签名,如下所示:
package javax.servlet;
import java.util.EventListener;
public interface ServletContextListener extends EventListener {
public void contextInitialized ( ServletContextEvent sce );
public void contextDestroyed ( ServletContextEvent sce );
}
通过实现这个接口,开发者可以在应用程序对象初始化和销毁时执行代码。为了使用这个接口,开发者需要创建一个实现类,并在 Web 配置文件中注册该类。在我们的示例中,我们创建了一个名为 StartupConfigurationListener 的类来实现这个接口,并将以下代码添加到 web.xml 中:
<listener>
<listener-class>
com.nealford.art.emotherearth.util.StartupConfigurationListener
</listener-class>
</listener>
StartupConfigurationListener 类负责初始化数据库连接池和边界类池,其代码如下:
package com.nealford.art.facade.emotherearth.util;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletContext;
import org.apache.commons.pool.impl.GenericKeyedObjectPool;
import java.sql.SQLException;
public class StartupConfigurationListener implements
ServletContextListener, AttributeConstants {
public void contextInitialized(ServletContextEvent sce) {
initializeDatabaseConnectionPool(sce.getServletContext());
BoundaryFacade.initializeBoundaryPool(
sce.getServletContext());
}
public void contextDestroyed(ServletContextEvent sce) {
}
private void initializeDatabaseConnectionPool(
ServletContext sc) {
DBPool dbPool = null;
try {
dbPool = createConnectionPool(sc);
} catch (SQLException sqlx) {
sc.log(new java.util.Date() + ":Connection pool error",
sqlx);
}
sc.setAttribute(DB_POOL, dbPool);
}
private DBPool createConnectionPool(ServletContext sc)
throws SQLException {
String driverClass = sc.getInitParameter(DRIVER_CLASS);
String password = sc.getInitParameter(PASSWORD);
String dbUrl = sc.getInitParameter(DB_URL);
String user = sc.getInitParameter(USER);
DBPool dbPool = null;
dbPool = new DBPool(driverClass, dbUrl, user, password);
return dbPool;
}
}
由于在应用程序退出时不需要进行特定的清理操作,因此我们将 StartupConfigurationListener 类中的 contextDestroyed() 方法留空,但该方法必须存在,因为它是接口的一部分。该类的主要操作在 contextInitialized() 方法中,该方法创建了 Web 应用程序所需的所有全局资源。而 GenericKeyedObjectPool 和支持类的实际细节则隐藏在外观类中。
建立外观类
外观类的主要职责是隐藏对象的获取和返回池的细节,以及池的设置信息。由于池化子系统的细节足够复杂,因此需要将其隐藏在应用程序主体之外,使得外观类成为唯一需要了解对象池工作原理的代码。
BoundaryFacade 类是一个单例类,这意味着所有需要使用它的其他类都共享一个实例。其声明和初始化方法如下:
public class BoundaryFacade implements AttributeConstants {
private static BoundaryFacade singleton;
private BoundaryFacade() {
}
public static BoundaryFacade getInstance() {
if (singleton == null)
singleton = new BoundaryFacade();
return singleton;
}
public void initializeBoundaryPool(ServletContext context) {
GenericKeyedObjectPool boundaryPool =
createBoundaryPool(context);
context.setAttribute(BOUNDARY_POOL, boundaryPool);
}
private GenericKeyedObjectPool.Config getPoolConfiguration(
ServletContext context) {
GenericKeyedObjectPool.Config conf =
new GenericKeyedObjectPool.Config();
conf.maxActive = Integer.parseInt(context.getInitParameter(
POOL_MAX_ACTIVE));
conf.whenExhaustedAction = Byte.parseByte(context.
getInitParameter(POOL_WHEN_EXHAUSTED));
return conf;
}
private GenericKeyedObjectPool createBoundaryPool(
ServletContext context) {
GenericKeyedObjectPool pool = null;
try {
pool = new GenericKeyedObjectPool(
new KeyedBoundaryPoolFactory());
pool.setConfig(getPoolConfiguration(context));
} catch (Throwable x) {
System.out.println("Pool creation exception: " +
x.getMessage());
x.printStackTrace();
}
return pool;
}
}
初始化 GenericKeyedObjectPool 的代码与之前章节中首次描述该类及其支持类使用的代码完全相同,唯一的区别在于代码的位置,它从启动 servlet 或监听器移到了这个外观类中。需要注意的是,这个类是一个经典的单例类,包含对自身的静态引用、私有构造函数和静态的 getInstance() 方法以允许访问。
使用外观模式借用对象
外观类中的 borrowOrderBoundary() 方法用于从池中返回 Order 实体的边界对象,其代码如下:
public OrderDb borrowOrderBoundary(HttpSession session) {
ServletContext sc = session.getServletContext();
OrderDb orderDb = null;
try {
GenericKeyedObjectPool boundaryPool =
(GenericKeyedObjectPool) sc.getAttribute(
BOUNDARY_POOL);
orderDb = (OrderDb) boundaryPool.borrowObject(OrderDb.class);
} catch (Exception x) {
session.getServletContext().log("Pool exception", x);
}
orderDb.setDBPool((DBPool) sc.getAttribute(DB_POOL));
return orderDb;
}
该方法的语义与之前章节中的类似,它从 servlet 上下文中检索边界池,并根据要返回的实体的类类型借用一个 OrderBoundary 类。在获取对象后,它将边界与数据库连接池的实例关联起来并返回。
应用程序中的另一个边界类是 ProductDb 类。从池中获取这个对象的工作方式与 OrderDb 边界类似,但为了提高效率,我们允许在用户会话中缓存 Product 边界。原因是用户在访问应用程序的目录页面时,需要多次访问产品信息,而目录页面是主要页面,用户大部分时间都会花费在这个页面上。因此,将该边界与用户关联起来是有意义的,同时这也体现了选择性缓存从池中获取的对象的灵活性。
为了返回 ProductDb 类的实例,我们有一个专门的方法 getProductBoundary(),其代码如下:
public ProductDb getProductBoundary(HttpSession session) {
ServletContext sc = session.getServletContext();
boolean cacheInBoundaryInSession =
Boolean.valueOf(sc.getInitParameter(
CACHE_BOUNDARY_IN_SESSION)).
booleanValue();
ProductDb productDb = null;
if (cacheInBoundaryInSession)
productDb = (ProductDb) session.getAttribute(
PRODUCT_BOUNDARY);
if (productDb == null) {
GenericKeyedObjectPool boundaryPool =
(GenericKeyedObjectPool) sc.getAttribute(
BOUNDARY_POOL);
try {
productDb = (ProductDb)
boundaryPool.borrowObject(ProductDb.class);
} catch (Throwable x) {
sc.log("Object pool exception", x);
}
productDb.setDBPool((DBPool) sc.getAttribute(DB_POOL));
if (cacheInBoundaryInSession)
session.setAttribute(PRODUCT_BOUNDARY, productDb);
}
return productDb;
}
该方法首先检查一个作为初始化参数设置的标志,以确定应用程序是否配置为在会话中缓存边界。将该标志放在配置文件中,只需编辑配置而无需重新编译应用程序,就可以轻松更改缓存策略,这也意味着非程序员也可以更改设置。如果启用了缓存,外观模式会尝试从会话中获取边界;如果失败,则从对象池中借用一个边界。在方法的最后,如果启用了缓存,它会将检索到的边界对象存储在会话中,以便下次访问。
使用外观模式返回对象
从池中借用的对象必须返回给池。在返回 Product 边界时,需要检查缓存标志的存在,并相应地返回边界对象。这项工作分为两个方法:一个根据会话中的缓存标志有条件地将对象返回给池,另一个则始终返回对象。这两个外观方法的代码如下:
public void returnProductBoundary(ProductDb productDb) {
try {
boundaryPool.returnObject(ProductDb.class,
productDb);
} catch (Throwable x) {
session.getServletContext().log(
"Pool return exception: " +
x.getMessage());
}
}
public void conditionallyReturnProductBoundary(
ProductDb productDb) {
ServletContext sc = session.getServletContext();
boolean cacheInBoundaryInSession =
Boolean.valueOf(sc.getInitParameter(
CACHE_BOUNDARY_IN_SESSION)).
booleanValue();
if (! cacheInBoundaryInSession && productDb != null) {
GenericKeyedObjectPool boundaryPool =
(GenericKeyedObjectPool)
session.getServletContext().getAttribute(
BOUNDARY_POOL);
returnProductBoundary(productDb);
}
}
returnProductBoundary() 方法是将借用的对象返回给池的典型代码。conditionallyReturnProductBoundary() 方法仅在对象未缓存在会话中时才返回对象。
将工作分为两个方法是为了处理用户在未完成订单的情况下离开应用程序的情况。正如 Web 开发者所知,Web 应用程序的用户可能随时失去兴趣,并且不会通知应用程序他们正在离开。会话超时最终会触发,移除会话,但移除会话只会使会话持有的对 Product 边界的引用无效,而不会将边界返回给池。为了解决这个问题,我们在应用程序中添加了另一个监听器,用于处理用户未正常完成与应用程序的交互,而是让会话超时的情况。
HttpSessionAttributeListener 接口是在 servlet 2.3 规范中引入的,它允许开发者将会话属性事件的事件处理程序附加到会话属性事件上。在这种情况下,我们将代码附加到 attributeRemoved() 方法上,以处理这种特殊情况。当会话超时时,它会移除所有属性并触发移除缓存的 Product 边界的代码。SessionScrubber 监听器的代码如下:
package com.nealford.art.facade.emotherearth.util;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import com.nealford.art.facade.emotherearth.boundary.ProductDb;
public class SessionScrubber implements
HttpSessionAttributeListener, AttributeConstants {
public void attributeAdded(HttpSessionBindingEvent se) {
}
public void attributeRemoved(HttpSessionBindingEvent se) {
ServletContext sc = se.getSession().getServletContext();
if (se.getName().equals(PRODUCT_BOUNDARY))
BoundaryFacade.getInstance().
returnProductBoundary((ProductDb) se.getValue());
}
public void attributeReplaced(HttpSessionBindingEvent se) {
}
}
虽然返回 Order 边界似乎不太复杂(因为它没有任何缓存选项),但解决方案并不像仅仅返回 Order 边界那么简单。Order 边界在用户与 Web 应用程序交互结束时使用。通常,当用户下完订单后,他们就完成了对网站的使用并准备离开,因此在这个会话中用户再次使用产品目录的可能性非常小。考虑到 Web 应用程序的使用情况,我们在返回 Order 边界的同时,如果 Product 边界缓存在会话中,也会将其返回。returnBoundaries() 方法的代码如下:
public void returnBoundaries(HttpSession session,
OrderDb orderDb) {
ServletContext sc = session.getServletContext();
boolean cacheInBoundaryInSession =
Boolean.valueOf(sc.getInitParameter(
CACHE_BOUNDARY_IN_SESSION)).
booleanValue();
ProductDb productDb =
(ProductDb) session.getAttribute(PRODUCT_BOUNDARY);
GenericKeyedObjectPool boundaryPool =
(GenericKeyedObjectPool) sc.getAttribute(
BOUNDARY_POOL);
try {
if (productDb != null && !cacheInBoundaryInSession)
boundaryPool.returnObject(ProductDb.class,
productDb);
if (orderDb != null)
boundaryPool.returnObject(OrderDb.class,
orderDb);
} catch (Exception x) {
sc.log("Pool exception", x);
}
}
该方法从会话中提取 Product 边界(如果存在),并在返回 Order 边界的同时将其返回。如果用户决定返回目录页面并下另一个订单,Product 边界将不再存在于用户的会话中,但外观模式会通过从对象池中借用另一个 Product 边界来无形地处理这种情况。
外观模式的优势
我们选择外观设计模式来实现缓存的目标是隐藏对象缓存和用户会话中缓存层的细节。我们成功地实现了这一目标,目录和结账控制器都得到了极大的简化。例如,简化后的结账控制器的 doPost() 方法代码如下:
public void doPost(HttpServletRequest request,
HttpServletResponse response) throws
ServletException, IOException {
RequestDispatcher dispatcher = null;
HttpSession session = redirectIfSessionNotPresent(
request, response);
String user = (String) session.getAttribute(USER);
ShoppingCart sc =
(ShoppingCart) session.getAttribute(CART);
BoundaryFacade facade = BoundaryFacade.getInstance();
OrderDb orderDb =
(OrderDb) facade.borrowBoundary(session, OrderDb.class);
Order order = createOrderFrom(request);
validateOrder(request, response, order);
addOrder(request, response, user, sc, orderDb,
order);
cleanUpUserResources(session);
buildConfirmationViewProperties(request, user, order);
forwardToConfirmation(request, response);
}
使用外观模式后,代码比之前的版本减少了 30 多行。更重要的是,新版本的控制器不再导入实现对象池的 Commons 库中的任何类。此外,SessionScrubber 监听器也使用外观模式来隐藏与池代码交互的细节。整个应用程序中唯一了解 Commons 池工作原理的代码都位于外观类中。
综上所述,享元模式和外观模式在缓存策略中各有其独特的优势和适用场景。享元模式适用于大量不可变对象的共享场景,能有效节省内存;而外观模式则擅长隐藏复杂子系统的细节,提高代码的可维护性和可使用性。在实际开发中,可以根据具体的业务需求和应用场景选择合适的缓存策略,以优化应用程序的性能和资源使用。
资源管理中的缓存策略
两种缓存策略对比
为了更清晰地了解享元模式和外观模式在缓存策略中的差异,我们可以通过以下表格进行对比:
| 对比项 | 享元模式 | 外观模式 |
| — | — | — |
| 核心目的 | 节省内存,通过共享对象减少存储成本 | 隐藏复杂子系统细节,提供简单易用的接口 |
| 适用场景 | 大量对象使用,对象不可变或状态可外部化,对象变化不频繁 | 复杂子系统交互,开发者无需了解底层细节 |
| 缓存特性 | 基于对象的状态信息进行缓存 | 封装缓存系统,对开发者透明 |
| 代码实现复杂度 | 关注对象状态管理和共享逻辑 | 集中管理对象池操作,简化业务代码 |
通过这个表格,我们可以清晰地看到两种模式在不同方面的特点,从而在实际应用中做出更合适的选择。
缓存策略的流程总结
为了帮助大家更好地理解整个缓存策略的操作流程,我们可以使用 mermaid 格式的流程图来展示:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A(应用启动):::process --> B(初始化缓存策略):::process
B --> C{选择缓存模式}:::process
C -->|享元模式| D(检查对象状态和共享条件):::process
D --> E(从缓存获取或创建对象):::process
C -->|外观模式| F(创建外观类管理对象池):::process
F --> G(通过外观类借用对象):::process
G --> H(使用对象进行业务操作):::process
H --> I{是否需要返回对象}:::process
I -->|是| J(根据缓存配置返回对象):::process
J --> K(对象返回池):::process
I -->|否| L(继续使用对象):::process
L --> H(使用对象进行业务操作):::process
这个流程图展示了从应用启动到对象使用和返回的整个过程,无论是享元模式还是外观模式,都遵循这个基本的流程框架。
缓存策略的实际应用建议
在实际开发中,如何选择合适的缓存策略是一个关键问题。以下是一些具体的建议:
享元模式应用建议
- 对象特性评估 :在考虑使用享元模式之前,需要仔细评估对象的特性。确保对象是大量使用的,并且对象的状态要么是不可变的,要么可以外部化。例如,在图形绘制应用中,大量的图形对象(如圆形、矩形等)如果具有相同的颜色、大小等属性,就可以考虑使用享元模式进行共享。
- 变化频率监测 :持续监测对象的变化频率。如果对象变化频繁,享元模式可能不适用,因为频繁的变化会破坏对象的共享性,导致缓存失效。可以通过日志记录或监控工具来跟踪对象的变化情况。
- 状态管理设计 :对于需要外部化状态的对象,要设计好状态管理机制。可以将外部状态存储在一个关联类中,确保状态的独立性和可维护性。
外观模式应用建议
- 子系统复杂度判断 :当面对复杂的子系统交互时,首先要判断子系统的复杂度是否达到需要使用外观模式的程度。如果子系统中的类之间存在大量的交互和依赖关系,导致开发者难以理解和使用,那么外观模式就是一个很好的选择。
- 接口设计优化 :在设计外观类的接口时,要注重接口的简洁性和易用性。接口应该能够满足开发者的常见需求,避免暴露过多的底层细节。可以通过用户调研和反馈来不断优化接口设计。
- 代码隔离与维护 :利用外观模式将复杂的缓存系统与业务代码隔离开来,提高代码的可维护性。在进行系统升级或修改时,只需要关注外观类的实现,而不会影响到业务代码的正常运行。
总结与展望
在软件开发中,缓存策略是优化资源使用和提高性能的重要手段。享元模式和外观模式作为两种不同的缓存策略,各自具有独特的优势和适用场景。享元模式通过共享对象节省内存,适用于大量不可变对象的场景;外观模式则通过隐藏复杂子系统细节,提供简单易用的接口,提高了代码的可维护性和可使用性。
在未来的开发中,随着软件系统的不断发展和复杂化,缓存策略的重要性将更加凸显。我们可以进一步探索如何将这两种模式结合使用,以应对更加复杂的业务需求。例如,在一个大型的电商应用中,可以使用享元模式来缓存商品的基本信息,同时使用外观模式来管理商品信息的获取和更新操作,从而实现更加高效的资源管理和性能优化。
同时,随着技术的不断进步,新的缓存技术和模式也可能会不断涌现。我们需要保持学习和探索的态度,不断更新自己的知识体系,以便在实际开发中能够灵活运用各种缓存策略,为用户提供更加优质的软件体验。
希望本文能够帮助大家更好地理解享元模式和外观模式在缓存策略中的应用,在实际开发中做出更加明智的选择。如果你在使用过程中遇到任何问题或有新的想法,欢迎在评论区留言讨论。
超级会员免费看

被折叠的 条评论
为什么被折叠?



