Tomcat中的JSP页面动态内容缓存:条件刷新策略
引言:动态内容缓存的痛点与解决方案
你是否曾面临这样的困境:Tomcat服务器上的JSP页面在高并发场景下响应缓慢,频繁的数据库查询和复杂业务逻辑计算导致服务器资源耗尽?当用户量激增时,传统的无缓存方案往往难以承受压力,而简单的静态缓存又无法满足动态内容的实时性需求。本文将系统介绍Tomcat环境下JSP页面动态内容缓存的实现方案,重点讲解条件刷新策略的设计与实践,帮助你在性能优化与数据实时性之间找到完美平衡点。
读完本文后,你将掌握:
- Tomcat容器级缓存与JSP页面缓存的工作原理
- 基于HTTP缓存头的客户端条件刷新实现
- 服务器端动态内容缓存的三种核心策略
- 结合监听器与自定义标签的高级缓存控制
- 缓存性能监控与调优的实战技巧
一、Tomcat中的JSP处理机制与缓存基础
1.1 JSP编译与执行流程
JSP(JavaServer Pages)是一种动态网页技术标准,它允许在HTML页面中嵌入Java代码。Tomcat作为主流的Servlet容器,通过Jasper引擎处理JSP页面,其完整流程如下:
JSP页面在首次被访问时会被编译为Servlet,后续请求直接调用已编译的Servlet类,除非检测到JSP文件发生修改。这一机制为缓存提供了基础——我们可以在不同阶段对处理结果进行缓存。
1.2 Tomcat缓存体系结构
Tomcat提供了多层次的缓存机制,从容器级到应用级再到页面级,形成了完整的缓存体系:
在conf/web.xml配置文件中,Tomcat默认配置了JspServlet,负责处理所有JSP请求:
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
JspServlet提供了若干与缓存相关的初始化参数,主要包括:
| 参数名 | 描述 | 默认值 | 缓存影响 |
|---|---|---|---|
| development | 是否启用开发模式 | true | 开发模式下会频繁检查JSP修改,影响缓存有效性 |
| checkInterval | 后台编译检查间隔(秒) | 0 | 非开发模式下生效,控制缓存刷新频率 |
| modificationTestInterval | 修改检查间隔(秒) | 4 | 开发模式下控制JSP重新编译检查频率 |
| maxLoadedJsps | 最大加载JSP数量 | -1(无限制) | 控制内存中缓存的JSP Servlet数量 |
| jspIdleTimeout | JSP闲置卸载时间(秒) | -1(永不卸载) | 控制JSP Servlet缓存的生存周期 |
1.3 缓存粒度与作用域
在设计JSP缓存策略时,需要根据内容特性选择合适的缓存粒度:
- 页面级缓存:缓存整个JSP页面的输出结果,适用于完全静态或很少变化的页面
- 片段级缓存:缓存页面中的特定区域,如导航栏、广告位等可复用组件
- 数据级缓存:缓存页面中使用的数据源,如数据库查询结果、API响应等
不同粒度的缓存在Tomcat中的实现方式和作用域各不相同:
| 缓存粒度 | 实现方式 | 作用域 | 优势 | 局限性 |
|---|---|---|---|---|
| 页面级 | HTTP缓存头、OutputCacheFilter | 应用级 | 实现简单,性能提升显著 | 无法区分页面中动态与静态部分 |
| 片段级 | 自定义标签、JSTL标签 | 请求级/会话级 | 灵活性高,可选择性缓存 | 增加页面复杂度 |
| 数据级 | 内存缓存、数据库缓存 | 应用级/会话级 | 针对性强,缓存命中率高 | 需要处理缓存一致性问题 |
二、基于HTTP协议的客户端条件刷新
2.1 HTTP缓存头工作原理
HTTP协议定义了一系列缓存相关的头部字段,允许客户端和服务器之间实现高效的缓存协作。对于动态生成的JSP页面,合理设置这些头部可以显著减少重复请求和数据传输量。
与JSP动态内容缓存相关的核心HTTP头字段包括:
- Cache-Control: 控制缓存行为的主要头部,可设置public/private、max-age、no-cache等指令
- ETag: 资源的实体标签,通常基于内容哈希或最后修改时间生成
- Last-Modified: 资源最后修改时间
- If-Modified-Since: 客户端发送的条件请求头,基于Last-Modified
- If-None-Match: 客户端发送的条件请求头,基于ETag
2.2 JSP页面中的缓存头设置
在JSP页面中,可以通过response对象动态设置HTTP缓存头,实现客户端条件刷新:
<%@ page import="java.util.Date" %>
<%
// 设置缓存有效期为1小时(3600秒)
response.setHeader("Cache-Control", "public, max-age=3600");
// 设置最后修改时间(模拟数据更新时间)
long lastModified = application.getAttribute("dataLastModified") != null ?
(Long) application.getAttribute("dataLastModified") : System.currentTimeMillis();
response.setDateHeader("Last-Modified", lastModified);
// 生成ETag(基于数据版本号)
String dataVersion = (String) application.getAttribute("dataVersion");
String etag = "\"" + dataVersion.hashCode() + "-" + lastModified + "\"";
response.setHeader("ETag", etag);
// 检查条件请求
String ifNoneMatch = request.getHeader("If-None-Match");
long ifModifiedSince = request.getDateHeader("If-Modified-Since");
if (ifNoneMatch != null && ifNoneMatch.equals(etag) ||
ifModifiedSince != -1 && ifModifiedSince >= lastModified) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return; // 终止页面处理,直接返回304
}
%>
<html>
<head>
<title>动态内容缓存示例</title>
</head>
<body>
<h1>产品列表(缓存版)</h1>
<p>最后更新时间: <%= new Date(lastModified) %></p>
<p>数据版本: <%= dataVersion %></p>
<!-- 产品列表内容 -->
</body>
</html>
上述代码实现了完整的HTTP条件请求逻辑:
- 设置Cache-Control头,告知客户端缓存有效期为1小时
- 从应用作用域获取数据最后修改时间和版本号
- 生成基于数据版本和修改时间的ETag
- 检查客户端发送的If-None-Match和If-Modified-Since头
- 如果资源未修改,返回304状态码并终止页面处理
2.3 缓存控制策略与最佳实践
根据内容的更新频率和重要性,可以采用不同的客户端缓存策略:
2.3.1 完全缓存策略
适用于长时间不变化的静态内容,如帮助页面、条款说明等:
<%
// 设置缓存有效期为7天
response.setHeader("Cache-Control", "public, max-age=604800");
// 设置过期时间(兼容HTTP/1.0)
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, 7);
response.setDateHeader("Expires", cal.getTimeInMillis());
%>
2.3.2 必须验证策略
适用于频繁变化但允许短时间缓存的内容,如新闻列表、产品目录等:
<%
// 缓存10分钟,但每次请求必须验证
response.setHeader("Cache-Control", "public, max-age=600, must-revalidate");
// 设置最后修改时间
long lastModified = productService.getLastUpdateTime();
response.setDateHeader("Last-Modified", lastModified);
// 检查条件请求
if (request.getDateHeader("If-Modified-Since") >= lastModified) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
%>
2.3.3 个性化内容缓存
对于用户特定的动态内容,如购物车、个人中心等,应使用私有缓存:
<%
// 仅客户端私有缓存,有效期15分钟
response.setHeader("Cache-Control", "private, max-age=900");
// 基于用户ID生成ETag
String userId = (String) session.getAttribute("userId");
String etag = "\"" + userId.hashCode() + "-" + System.currentTimeMillis()/900000 + "\"";
response.setHeader("ETag", etag);
// 检查条件请求
if (etag.equals(request.getHeader("If-None-Match"))) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
%>
三、Tomcat服务器端动态内容缓存策略
3.1 容器级缓存配置
Tomcat提供了若干容器级别的缓存配置选项,可以在conf/context.xml或应用的META-INF/context.xml中进行设置:
<Context>
<!-- 默认资源监控配置 -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
<!-- 配置输出缓冲大小(默认为8KB) -->
<Valve className="org.apache.catalina.valves.OutputBufferValve" bufferSize="32768" />
<!-- 启用静态资源缓存 -->
<Resources cachingAllowed="true" cacheMaxSize="10485760" />
<!-- 配置JSP引擎缓存参数 -->
<Parameter name="org.apache.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING" value="false" override="false"/>
<Parameter name="org.apache.jasper.runtime.BodyContentImpl.BUFFER_SIZE" value="16384" override="false"/>
</Context>
其中与缓存相关的关键配置项:
- cachingAllowed: 是否允许静态资源缓存,默认为false
- cacheMaxSize: 缓存的最大大小(字节),默认为10MB
- cacheTTL: 缓存项的生存时间(毫秒),适用于所有缓存资源
- bufferSize: 响应输出缓冲区大小,较大的缓冲区有助于提升性能
3.2 基于Servlet的JSP片段缓存
对于JSP页面中可复用的动态片段,可以通过自定义Servlet实现服务器端缓存:
package com.example.cache;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/cachedFragment")
public class CachedFragmentServlet extends HttpServlet {
// 内存缓存存储
private final Map<String, CacheEntry> cache = new HashMap<>();
// 缓存过期时间(分钟)
private static final int CACHE_TTL = 10;
// 缓存条目内部类
private static class CacheEntry {
String content;
long timestamp;
CacheEntry(String content) {
this.content = content;
this.timestamp = System.currentTimeMillis();
}
boolean isExpired() {
return System.currentTimeMillis() - timestamp > TimeUnit.MINUTES.toMillis(CACHE_TTL);
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String fragment = request.getParameter("fragment");
if (fragment == null || fragment.isEmpty()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Fragment parameter required");
return;
}
// 尝试从缓存获取
CacheEntry entry = cache.get(fragment);
if (entry != null && !entry.isExpired()) {
response.getWriter().write(entry.content);
return;
}
// 缓存未命中,生成片段内容
StringWriter sw = new StringWriter();
RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/fragments/" + fragment + ".jspf");
rd.include(request, new HttpServletResponseWrapper(response) {
@Override
public PrintWriter getWriter() {
return new PrintWriter(sw);
}
@Override
public ServletOutputStream getOutputStream() {
return new ServletOutputStream() {
@Override
public void write(int b) {
sw.write(b);
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener listener) {}
};
}
});
// 存入缓存
String content = sw.toString();
cache.put(fragment, new CacheEntry(content));
// 输出内容
response.getWriter().write(content);
}
// 提供清除缓存的方法
public void clearCache(String fragment) {
if (fragment == null) {
cache.clear();
} else {
cache.remove(fragment);
}
}
}
在JSP页面中使用该Servlet包含缓存片段:
<!-- 引入缓存的产品分类菜单 -->
<jsp:include page="/cachedFragment?fragment=productCategories" flush="true" />
<!-- 引入缓存的热门商品列表 -->
<jsp:include page="/cachedFragment?fragment=hotProducts" flush="true" />
3.3 使用监听器实现缓存失效机制
为了解决缓存内容与数据源同步的问题,可以使用监听器(Listener)监听数据变更事件,及时清除相关缓存:
package com.example.cache;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
@WebListener
public class CacheInvalidationListener implements ServletContextListener, HttpSessionListener {
private CachedFragmentServlet fragmentCache;
@Override
public void contextInitialized(ServletContextEvent event) {
// 获取缓存Servlet实例
ServletContext context = event.getServletContext();
fragmentCache = (CachedFragmentServlet) context.getAttribute("cachedFragmentServlet");
// 注册数据变更监听器
ProductService productService = (ProductService) context.getAttribute("productService");
productService.addDataChangeListener(new DataChangeListener() {
@Override
public void onDataChanged(String dataType) {
// 根据数据类型清除相关缓存
if ("category".equals(dataType)) {
fragmentCache.clearCache("productCategories");
} else if ("product".equals(dataType)) {
fragmentCache.clearCache("hotProducts");
} else {
fragmentCache.clearCache(null); // 清除所有缓存
}
// 更新应用作用域中的最后修改时间
context.setAttribute("dataLastModified", System.currentTimeMillis());
}
});
}
// 其他监听器方法实现...
}
3.4 基于EHCache的高级缓存实现
对于复杂的缓存需求,推荐使用专业的缓存框架如EHCache。首先在pom.xml中添加依赖:
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.10.0</version>
</dependency>
创建EHCache配置文件src/main/resources/ehcache.xml:
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.ehcache.org/v3"
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">
<cache alias="jspFragments">
<key-type>java.lang.String</key-type>
<value-type>java.lang.String</value-type>
<expiry>
<ttl unit="minutes">10</ttl>
</expiry>
<resources>
<heap unit="entries">100</heap>
<offheap unit="MB">10</offheap>
</resources>
</cache>
<cache alias="productData">
<key-type>java.lang.Long</key-type>
<value-type>com.example.Product</value-type>
<expiry>
<ttl unit="minutes">5</ttl>
</expiry>
<resources>
<heap unit="entries">1000</heap>
<offheap unit="MB">50</offheap>
</resources>
</cache>
</config>
创建缓存管理器工具类:
package com.example.cache;
import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.Configuration;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.xml.XmlConfiguration;
import java.net.URI;
import java.net.URISyntaxException;
public class CacheManagerUtil {
private static CacheManager cacheManager;
private CacheManagerUtil() {}
public static synchronized CacheManager getCacheManager() {
if (cacheManager == null) {
try {
URI uri = CacheManagerUtil.class.getClassLoader().getResource("ehcache.xml").toURI();
Configuration config = new XmlConfiguration(uri);
cacheManager = CacheManagerBuilder.newCacheManager(config);
cacheManager.init();
} catch (URISyntaxException e) {
throw new RuntimeException("Failed to initialize cache manager", e);
}
}
return cacheManager;
}
@SuppressWarnings("unchecked")
public static <K, V> Cache<K, V> getCache(String cacheName) {
return (Cache<K, V>) getCacheManager().getCache(cacheName);
}
public static void shutdown() {
if (cacheManager != null) {
cacheManager.close();
}
}
}
在JSP页面中使用EHCache缓存动态数据:
<%@ page import="com.example.cache.CacheManagerUtil" %>
<%@ page import="org.ehcache.Cache" %>
<%@ page import="com.example.Product" %>
<%
Long productId = Long.parseLong(request.getParameter("id"));
Cache<Long, Product> productCache = CacheManagerUtil.getCache("productData");
// 尝试从缓存获取产品数据
Product product = productCache.get(productId);
if (product == null) {
// 缓存未命中,从数据库获取
product = productService.getProductById(productId);
// 存入缓存
if (product != null) {
productCache.put(productId, product);
}
}
if (product == null) {
// 处理产品不存在的情况
%>
<div class="error">产品不存在或已下架</div>
<%
return;
}
%>
<div class="product-detail">
<h1><%= product.getName() %></h1>
<p class="price">¥<%= product.getPrice() %></p>
<div class="description"><%= product.getDescription() %></div>
<!-- 其他产品信息 -->
</div>
四、缓存控制高级技术
4.1 基于JSP标签的缓存控制
创建自定义JSP标签实现声明式缓存控制,可以显著提高代码复用性和可维护性:
package com.example.tags;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;
import java.io.IOException;
import java.io.StringWriter;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
public class CacheTag extends BodyTagSupport {
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
private String key;
private int ttl = 5; // 默认5分钟
private String scope = "application"; // application, session, request
private static class CacheEntry {
String content;
long expiryTime;
CacheEntry(String content, int ttl) {
this.content = content;
this.expiryTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(ttl);
}
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
}
@Override
public int doStartTag() throws JspException {
// 生成完整缓存键
String cacheKey = generateCacheKey();
// 检查缓存
CacheEntry entry = cache.get(cacheKey);
if (entry != null && !entry.isExpired()) {
// 缓存命中,输出内容
try {
pageContext.getOut().write(entry.content);
} catch (IOException e) {
throw new JspException("Error writing cached content", e);
}
return SKIP_BODY; // 跳过标签体执行
}
// 缓存未命中,继续执行标签体
return EVAL_BODY_BUFFERED;
}
@Override
public int doEndTag() throws JspException {
// 生成完整缓存键
String cacheKey = generateCacheKey();
// 获取标签体内容
String content = bodyContent.getString();
// 存入缓存
cache.put(cacheKey, new CacheEntry(content, ttl));
// 输出内容
try {
pageContext.getOut().write(content);
} catch (IOException e) {
throw new JspException("Error writing tag content", e);
}
return EVAL_PAGE;
}
private String generateCacheKey() {
StringBuilder keyBuilder = new StringBuilder();
// 根据作用域添加前缀
switch (scope.toLowerCase()) {
case "session":
keyBuilder.append(pageContext.getSession().getId()).append(":");
break;
case "request":
keyBuilder.append(pageContext.getRequest().getAttribute("javax.servlet.include.request_uri")).append(":");
keyBuilder.append(UUID.randomUUID().toString()).append(":");
break;
default: // application
keyBuilder.append("app:");
}
// 添加用户指定的key
keyBuilder.append(key != null ? key : "default");
return keyBuilder.toString();
}
// 静态方法,用于清除缓存
public static void clearCache(String keyPattern) {
if (keyPattern == null) {
cache.clear();
} else {
cache.keySet().removeIf(key -> key.contains(keyPattern));
}
}
// getter和setter方法
public void setKey(String key) {
this.key = key;
}
public void setTtl(int ttl) {
this.ttl = ttl;
}
public void setScope(String scope) {
this.scope = scope;
}
}
创建标签库描述文件/WEB-INF/tags/cache.tld:
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
version="2.1">
<tlib-version>1.0</tlib-version>
<short-name>cache</short-name>
<uri>http://example.com/tags/cache</uri>
<tag>
<name>cache</name>
<tag-class>com.example.tags.CacheTag</tag-class>
<body-content>JSP</body-content>
<attribute>
<name>key</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
<type>java.lang.String</type>
</attribute>
<attribute>
<name>ttl</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
<type>java.lang.Integer</type>
</attribute>
<attribute>
<name>scope</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
<type>java.lang.String</type>
<description>缓存作用域: application, session, request</description>
<validator>
<validator-class>com.example.tags.ScopeValidator</validator-class>
</validator>
</attribute>
</tag>
</taglib>
在JSP页面中使用缓存标签:
<%@ taglib prefix="cache" uri="http://example.com/tags/cache" %>
<!-- 应用级缓存,缓存10分钟 -->
<cache:cache key="category_list" ttl="10" scope="application">
<div class="category-list">
<%
List<Category> categories = categoryService.getAllCategories();
for (Category category : categories) {
%>
<a href="/category/<%= category.getId() %>"><%= category.getName() %></a>
<% } %>
</div>
</cache:cache>
<!-- 用户会话级缓存,缓存5分钟 -->
<cache:cache key="user_recommendations_<%= userId %>" ttl="5" scope="session">
<div class="recommendations">
<h3>为您推荐</h3>
<%
List<Product> recommendations = recommendationService.getUserRecommendations(userId);
for (Product product : recommendations) {
%>
<div class="product-item">
<h4><%= product.getName() %></h4>
<p>¥<%= product.getPrice() %></p>
</div>
<% } %>
</div>
</cache:cache>
4.2 结合Spring框架的高级缓存
如果项目中使用了Spring框架,可以利用其提供的缓存抽象实现更强大的缓存功能。首先在Spring配置中启用缓存:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- 启用缓存注解 -->
<cache:annotation-driven cache-manager="cacheManager"/>
<!-- 配置EHCache缓存管理器 -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcacheManager"/>
</bean>
<bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="/WEB-INF/ehcache-spring.xml"/>
<property name="shared" value="true"/>
</bean>
</beans>
在服务层使用缓存注解:
package com.example.service;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.example.Product;
import com.example.dao.ProductDao;
import javax.annotation.Resource;
import java.util.List;
@Service
public class ProductServiceImpl implements ProductService {
@Resource
private ProductDao productDao;
@Override
@Cacheable(value = "products", key = "#id", unless = "#result == null")
public Product getProductById(Long id) {
// 方法执行前会检查缓存,缓存未命中时才执行实际查询
return productDao.findById(id);
}
@Override
@Cacheable(value = "products", key = "'category_'+#categoryId")
public List<Product> getProductsByCategory(Long categoryId) {
return productDao.findByCategoryId(categoryId);
}
@Override
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
// 更新产品信息,并将结果存入缓存
return productDao.update(product);
}
@Override
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productDao.delete(id);
}
@Override
@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() {
// 清除products缓存中的所有条目
}
@Override
@CacheEvict(value = "products", key = "'category_'+#categoryId")
public void clearCategoryCache(Long categoryId) {
// 清除特定分类的产品缓存
}
}
在JSP页面中通过Spring的JSP标签访问缓存数据:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!-- 使用Spring标签获取缓存的产品信息 -->
<spring:eval expression="@productService.getProductById(productId)" var="product" />
<c:if test="${not empty product}">
<div class="product-detail">
<h1>${product.name}</h1>
<p class="price">¥${product.price}</p>
<div class="description">${product.description}</div>
</div>
</c:if>
<!-- 获取缓存的分类产品列表 -->
<spring:eval expression="@productService.getProductsByCategory(categoryId)" var="products" />
<div class="product-list">
<c:forEach items="${products}" var="product">
<div class="product-item">
<h3>${product.name}</h3>
<p>¥${product.price}</p>
<a href="/product/${product.id}">查看详情</a>
</div>
</c:forEach>
</div>
五、缓存性能监控与调优
5.1 缓存监控指标与实现
为了评估缓存效果并进行优化,需要监控关键的缓存指标,主要包括:
- 缓存命中率:(缓存命中次数 / 总请求次数) × 100%
- 缓存失效次数:缓存过期或被主动清除的次数
- 平均缓存生存时间:缓存条目从创建到失效的平均时间
- 缓存大小:当前缓存的条目数量和占用空间
实现一个简单的缓存监控工具类:
package com.example.cache;
import java.util.concurrent.atomic.AtomicLong;
public class CacheMonitor {
private final String cacheName;
private final AtomicLong hits = new AtomicLong(0);
private final AtomicLong misses = new AtomicLong(0);
private final AtomicLong evictions = new AtomicLong(0);
private final AtomicLong expiryCount = new AtomicLong(0);
public CacheMonitor(String cacheName) {
this.cacheName = cacheName;
}
public void recordHit() {
hits.incrementAndGet();
}
public void recordMiss() {
misses.incrementAndGet();
}
public void recordEviction() {
evictions.incrementAndGet();
}
public void recordExpiry() {
expiryCount.incrementAndGet();
}
public double getHitRate() {
long total = hits.get() + misses.get();
return total == 0 ? 0 : (double) hits.get() / total;
}
public CacheStats getStats() {
return new CacheStats(
cacheName,
hits.get(),
misses.get(),
getHitRate(),
evictions.get(),
expiryCount.get()
);
}
public static class CacheStats {
private final String cacheName;
private final long hits;
private final long misses;
private final double hitRate;
private final long evictions;
private final long expiries;
public CacheStats(String cacheName, long hits, long misses, double hitRate,
long evictions, long expiries) {
this.cacheName = cacheName;
this.hits = hits;
this.misses = misses;
this.hitRate = hitRate;
this.evictions = evictions;
this.expiries = expiries;
}
// getter方法
public String getCacheName() { return cacheName; }
public long getHits() { return hits; }
public long getMisses() { return misses; }
public double getHitRate() { return hitRate; }
public long getEvictions() { return evictions; }
public long getExpiries() { return expiries; }
}
}
修改前面的缓存标签,添加监控功能:
public class MonitoredCacheTag extends CacheTag {
private static final CacheMonitor monitor = new CacheMonitor("jsp_fragments");
@Override
public int doStartTag() throws JspException {
String cacheKey = generateCacheKey();
CacheEntry entry = cache.get(cacheKey);
if (entry != null && !entry.isExpired()) {
monitor.recordHit(); // 记录缓存命中
try {
pageContext.getOut().write(entry.content);
} catch (IOException e) {
throw new JspException("Error writing cached content", e);
}
return SKIP_BODY;
} else {
monitor.recordMiss(); // 记录缓存未命中
if (entry != null && entry.isExpired()) {
monitor.recordExpiry(); // 记录缓存过期
}
return EVAL_BODY_BUFFERED;
}
}
@Override
public static void clearCache(String keyPattern) {
int sizeBefore = cache.size();
if (keyPattern == null) {
cache.clear();
monitor.recordEviction();
} else {
cache.keySet().removeIf(key -> key.contains(keyPattern));
if (cache.size() < sizeBefore) {
monitor.recordEviction();
}
}
}
public static CacheMonitor.CacheStats getStats() {
return monitor.getStats();
}
}
创建一个监控页面查看缓存统计信息:
<%@ page import="com.example.tags.MonitoredCacheTag" %>
<%@ page import="com.example.cache.CacheManagerUtil" %>
<%@ page import="org.ehcache.Cache" %>
<%@ page import="org.ehcache.stats.CacheStatistics" %>
<%@ page import="org.ehcache.stats.StatisticsService" %>
<%@ page import="org.ehcache.core.statistics.DefaultStatisticsService" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>缓存监控面板</title>
<style>
table { border-collapse: collapse; margin: 20px 0; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
.stats-container { margin: 20px; padding: 15px; border: 1px solid #ccc; border-radius: 5px; }
.hit-rate-high { color: green; }
.hit-rate-medium { color: orange; }
.hit-rate-low { color: red; }
</style>
</head>
<body>
<h1>Tomcat JSP缓存监控面板</h1>
<div class="stats-container">
<h2>1. JSP片段缓存统计</h2>
<%
MonitoredCacheTag.CacheStats jspStats = MonitoredCacheTag.getStats();
String hitRateClass = jspStats.getHitRate() > 0.7 ? "hit-rate-high" :
(jspStats.getHitRate() > 0.4 ? "hit-rate-medium" : "hit-rate-low");
%>
<table>
<tr>
<th>缓存名称</th>
<th>命中次数</th>
<th>未命中次数</th>
<th>命中率</th>
<th>过期次数</th>
<th>清除次数</th>
</tr>
<tr>
<td><%= jspStats.getCacheName() %></td>
<td><%= jspStats.getHits() %></td>
<td><%= jspStats.getMisses() %></td>
<td class="<%= hitRateClass %>"><%= String.format("%.2f%%", jspStats.getHitRate() * 100) %></td>
<td><%= jspStats.getExpiries() %></td>
<td><%= jspStats.getEvictions() %></td>
</tr>
</table>
</div>
<div class="stats-container">
<h2>2. 产品数据缓存统计</h2>
<%
Cache<Long, Product> productCache = CacheManagerUtil.getCache("productData");
StatisticsService statsService = new DefaultStatisticsService();
CacheStatistics productStats = statsService.getCacheStatistics(productCache);
String productHitRateClass = productStats.getHitRate() > 0.7 ? "hit-rate-high" :
(productStats.getHitRate() > 0.4 ? "hit-rate-medium" : "hit-rate-low");
%>
<table>
<tr>
<th>缓存名称</th>
<th>命中次数</th>
<th>未命中次数</th>
<th>命中率</th>
<th>缓存条目数</th>
<th>平均加载时间(ms)</th>
</tr>
<tr>
<td>productData</td>
<td><%= productStats.getCacheHits() %></td>
<td><%= productStats.getCacheMisses() %></td>
<td class="<%= productHitRateClass %>"><%= String.format("%.2f%%", productStats.getHitRate() * 100) %></td>
<td><%= productStats.getEntryCount() %></td>
<td><%= productStats.getAverageLoadTime() %></td>
</tr>
</table>
</div>
<div class="stats-container">
<h2>3. 缓存性能建议</h2>
<ul>
<% if (jspStats.getHitRate() < 0.4) { %>
<li><strong>警告:</strong> JSP片段缓存命中率低于40%,建议检查缓存键设计或增加缓存时间</li>
<% } %>
<% if (productStats.getEntryCount() > 1000) { %>
<li><strong>注意:</strong> 产品数据缓存条目超过1000,考虑增加缓存大小或优化缓存策略</li>
<% } %>
<% if (productStats.getAverageLoadTime() > 50) { %>
<li><strong>优化建议:</strong> 产品数据加载平均时间超过50ms,建议优化数据库查询</li>
<% } %>
</ul>
</div>
</body>
</html>
5.2 缓存调优策略与最佳实践
缓存调优是一个持续迭代的过程,需要根据应用的实际运行情况进行调整。以下是一些经过实践验证的缓存调优策略:
5.2.1 缓存命中率提升技巧
-
合理设置缓存粒度:
- 将频繁访问但很少变化的内容独立缓存
- 避免缓存包含用户特定信息的页面片段
- 对大型页面实施片段级缓存而非整体缓存
-
优化缓存键设计:
// 不佳的缓存键设计 String key = "product_" + productId + "_" + userId + "_" + locale; // 优化后的缓存键设计 // 1. 将用户无关部分独立缓存 String baseKey = "product_" + productId; // 2. 用户特定部分单独处理或使用私有缓存 String userKey = "user_" + userId + "_locale_" + locale; -
实现缓存预热:
@WebListener public class CachePreloader implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent event) { // 在应用启动时预加载热门数据到缓存 new Thread(() -> { ProductService productService = (ProductService) event.getServletContext() .getAttribute("productService"); List<Long> hotProductIds = Arrays.asList(1001L, 1002L, 1003L, 1004L); for (Long id : hotProductIds) { productService.getProductById(id); // 触发缓存加载 } }).start(); } }
5.2.2 缓存失效策略优化
-
基于事件的主动失效:
// 产品数据变更时主动更新缓存 public class ProductDataPublisher { private List<CacheInvalidationListener> listeners = new CopyOnWriteArrayList<>(); public void registerListener(CacheInvalidationListener listener) { listeners.add(listener); } public void publishProductUpdate(Product product) { for (CacheInvalidationListener listener : listeners) { listener.onProductUpdated(product.getId()); listener.onCategoryUpdated(product.getCategoryId()); } } } -
分层缓存失效:
-
缓存穿透防护:
@Cacheable(value = "products", key = "#id") public Product getProductById(Long id) { Product product = productDao.findById(id); if (product == null) { // 缓存空结果,设置较短TTL return new NullProduct(id); // 特殊的空产品对象 } return product; }
5.2.3 缓存并发控制
在高并发场景下,缓存可能成为新的瓶颈,需要实施适当的并发控制策略:
-
缓存锁机制:
private final ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>(); public Product getProductWithLock(Long id) { String key = "product_" + id; Product product = cache.get(key); if (product == null) { // 获取或创建锁对象 Lock lock = locks.computeIfAbsent(key, k -> new ReentrantLock()); try { lock.lock(); // 双重检查 product = cache.get(key); if (product == null) { product = productDao.findById(id); cache.put(key, product); } } finally { lock.unlock(); // 移除锁对象 locks.remove(key); } } return product; } -
使用读写锁分离读和写:
private final ConcurrentHashMap<String, ReadWriteLock> rwLocks = new ConcurrentHashMap<>(); public Product getProductWithRWLock(Long id) { String key = "product_" + id; ReadWriteLock rwLock = rwLocks.computeIfAbsent(key, k -> new ReentrantReadWriteLock()); // 读操作使用读锁 rwLock.readLock().lock(); try { Product product = cache.get(key); if (product != null) { return product; } } finally { rwLock.readLock().unlock(); } // 缓存未命中,获取写锁 rwLock.writeLock().lock(); try { // 双重检查 Product product = cache.get(key); if (product == null) { product = productDao.findById(id); cache.put(key, product); } return product; } finally { rwLock.writeLock().unlock(); } }
5.2.4 缓存与数据库一致性保障
-
写透缓存模式:
@Transactional public Product updateProduct(Product product) { // 1. 先更新数据库 Product updated = productDao.update(product); // 2. 再更新缓存 cache.put("product_" + product.getId(), updated); // 3. 清除相关缓存 cache.remove("category_" + product.getCategoryId()); return updated; } -
延迟双删策略:
@Transactional public void updateProductWithDelayDelete(Product product) { // 1. 先删除缓存 cache.remove("product_" + product.getId()); // 2. 更新数据库 productDao.update(product); // 3. 延迟一段时间后再次删除缓存 scheduler.schedule(() -> { cache.remove("product_" + product.getId()); cache.remove("category_" + product.getCategoryId()); }, 100, TimeUnit.MILLISECONDS); }
六、总结与展望
本文详细介绍了Tomcat环境下JSP页面动态内容缓存的条件刷新策略,从HTTP协议缓存机制到Tomcat容器级缓存配置,再到应用级缓存实现,全面覆盖了JSP缓存的各个方面。通过合理运用这些技术,可以显著提升Java Web应用的性能和可扩展性。
主要内容总结:
- 缓存基础:理解JSP编译流程和Tomcat缓存体系是实现有效缓存的基础
- 客户端缓存:利用HTTP缓存头实现条件刷新,减少服务器负载和网络传输
- 服务器端缓存:通过容器配置、自定义Servlet和标签库实现多级缓存
- 高级缓存技术:结合Spring框架和专业缓存库实现企业级缓存方案
- 缓存监控与调优:建立缓存监控体系,持续优化缓存策略
随着微服务和云原生架构的兴起,未来的缓存技术将更加注重分布式缓存协调、云环境下的弹性缓存资源管理,以及基于AI的智能缓存预测和自动调优。掌握本文介绍的缓存基础和实践技巧,将为应对这些新兴挑战奠定坚实基础。
最后,记住缓存是一把双刃剑——合理使用可以显著提升性能,但过度使用或不当使用可能导致数据一致性问题和系统复杂度增加。最佳实践是从业务需求出发,选择合适的缓存策略,并通过充分的测试和监控确保缓存系统的稳定性和有效性。
附录:Tomcat缓存配置速查表
| 配置项 | 位置 | 默认值 | 说明 |
|---|---|---|---|
| cachingAllowed | context.xml | false | 是否允许静态资源缓存 |
| cacheMaxSize | context.xml | 10485760(10MB) | 静态资源缓存最大大小 |
| development | web.xml(JspServlet) | true | JSP开发模式开关 |
| checkInterval | web.xml(JspServlet) | 0 | JSP后台检查间隔(秒) |
| modificationTestInterval | web.xml(JspServlet) | 4 | JSP修改检查间隔(秒) |
| maxLoadedJsps | web.xml(JspServlet) | -1 | 最大加载JSP数量 |
| org.apache.jasper.runtime.BodyContentImpl.BUFFER_SIZE | context.xml | 8192 | JSP输出缓冲区大小 |
| org.apache.catalina.STRICT_SERVLET_COMPLIANCE | context.xml | false | 是否严格遵循Servlet规范 |
缓存命中率目标参考值:
- 页面级缓存:>70%
- 数据级缓存:>85%
- 片段级缓存:>60%
当缓存命中率低于上述阈值时,建议重新评估缓存策略和实现方式。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



