Tomcat中的JSP页面动态内容缓存:条件刷新策略

Tomcat中的JSP页面动态内容缓存:条件刷新策略

【免费下载链接】tomcat Tomcat是一个开源的Web服务器,主要用于部署Java Web应用程序。它的特点是易用性高、稳定性好、兼容性广等。适用于Java Web应用程序部署场景。 【免费下载链接】tomcat 项目地址: https://gitcode.com/gh_mirrors/tom/tomcat

引言:动态内容缓存的痛点与解决方案

你是否曾面临这样的困境:Tomcat服务器上的JSP页面在高并发场景下响应缓慢,频繁的数据库查询和复杂业务逻辑计算导致服务器资源耗尽?当用户量激增时,传统的无缓存方案往往难以承受压力,而简单的静态缓存又无法满足动态内容的实时性需求。本文将系统介绍Tomcat环境下JSP页面动态内容缓存的实现方案,重点讲解条件刷新策略的设计与实践,帮助你在性能优化与数据实时性之间找到完美平衡点。

读完本文后,你将掌握:

  • Tomcat容器级缓存与JSP页面缓存的工作原理
  • 基于HTTP缓存头的客户端条件刷新实现
  • 服务器端动态内容缓存的三种核心策略
  • 结合监听器与自定义标签的高级缓存控制
  • 缓存性能监控与调优的实战技巧

一、Tomcat中的JSP处理机制与缓存基础

1.1 JSP编译与执行流程

JSP(JavaServer Pages)是一种动态网页技术标准,它允许在HTML页面中嵌入Java代码。Tomcat作为主流的Servlet容器,通过Jasper引擎处理JSP页面,其完整流程如下:

mermaid

JSP页面在首次被访问时会被编译为Servlet,后续请求直接调用已编译的Servlet类,除非检测到JSP文件发生修改。这一机制为缓存提供了基础——我们可以在不同阶段对处理结果进行缓存。

1.2 Tomcat缓存体系结构

Tomcat提供了多层次的缓存机制,从容器级到应用级再到页面级,形成了完整的缓存体系:

mermaid

在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数量
jspIdleTimeoutJSP闲置卸载时间(秒)-1(永不卸载)控制JSP Servlet缓存的生存周期

1.3 缓存粒度与作用域

在设计JSP缓存策略时,需要根据内容特性选择合适的缓存粒度:

  1. 页面级缓存:缓存整个JSP页面的输出结果,适用于完全静态或很少变化的页面
  2. 片段级缓存:缓存页面中的特定区域,如导航栏、广告位等可复用组件
  3. 数据级缓存:缓存页面中使用的数据源,如数据库查询结果、API响应等

不同粒度的缓存在Tomcat中的实现方式和作用域各不相同:

缓存粒度实现方式作用域优势局限性
页面级HTTP缓存头、OutputCacheFilter应用级实现简单,性能提升显著无法区分页面中动态与静态部分
片段级自定义标签、JSTL标签请求级/会话级灵活性高,可选择性缓存增加页面复杂度
数据级内存缓存、数据库缓存应用级/会话级针对性强,缓存命中率高需要处理缓存一致性问题

二、基于HTTP协议的客户端条件刷新

2.1 HTTP缓存头工作原理

HTTP协议定义了一系列缓存相关的头部字段,允许客户端和服务器之间实现高效的缓存协作。对于动态生成的JSP页面,合理设置这些头部可以显著减少重复请求和数据传输量。

mermaid

与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条件请求逻辑:

  1. 设置Cache-Control头,告知客户端缓存有效期为1小时
  2. 从应用作用域获取数据最后修改时间和版本号
  3. 生成基于数据版本和修改时间的ETag
  4. 检查客户端发送的If-None-Match和If-Modified-Since头
  5. 如果资源未修改,返回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 缓存命中率提升技巧
  1. 合理设置缓存粒度

    • 将频繁访问但很少变化的内容独立缓存
    • 避免缓存包含用户特定信息的页面片段
    • 对大型页面实施片段级缓存而非整体缓存
  2. 优化缓存键设计

    // 不佳的缓存键设计
    String key = "product_" + productId + "_" + userId + "_" + locale;
    
    // 优化后的缓存键设计
    // 1. 将用户无关部分独立缓存
    String baseKey = "product_" + productId;
    // 2. 用户特定部分单独处理或使用私有缓存
    String userKey = "user_" + userId + "_locale_" + locale;
    
  3. 实现缓存预热

    @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 缓存失效策略优化
  1. 基于事件的主动失效

    // 产品数据变更时主动更新缓存
    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());
            }
        }
    }
    
  2. 分层缓存失效mermaid

  3. 缓存穿透防护

    @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 缓存并发控制

在高并发场景下,缓存可能成为新的瓶颈,需要实施适当的并发控制策略:

  1. 缓存锁机制

    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;
    }
    
  2. 使用读写锁分离读和写

    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 缓存与数据库一致性保障
  1. 写透缓存模式

    @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;
    }
    
  2. 延迟双删策略

    @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应用的性能和可扩展性。

主要内容总结:

  1. 缓存基础:理解JSP编译流程和Tomcat缓存体系是实现有效缓存的基础
  2. 客户端缓存:利用HTTP缓存头实现条件刷新,减少服务器负载和网络传输
  3. 服务器端缓存:通过容器配置、自定义Servlet和标签库实现多级缓存
  4. 高级缓存技术:结合Spring框架和专业缓存库实现企业级缓存方案
  5. 缓存监控与调优:建立缓存监控体系,持续优化缓存策略

随着微服务和云原生架构的兴起,未来的缓存技术将更加注重分布式缓存协调、云环境下的弹性缓存资源管理,以及基于AI的智能缓存预测和自动调优。掌握本文介绍的缓存基础和实践技巧,将为应对这些新兴挑战奠定坚实基础。

最后,记住缓存是一把双刃剑——合理使用可以显著提升性能,但过度使用或不当使用可能导致数据一致性问题和系统复杂度增加。最佳实践是从业务需求出发,选择合适的缓存策略,并通过充分的测试和监控确保缓存系统的稳定性和有效性。

附录:Tomcat缓存配置速查表

配置项位置默认值说明
cachingAllowedcontext.xmlfalse是否允许静态资源缓存
cacheMaxSizecontext.xml10485760(10MB)静态资源缓存最大大小
developmentweb.xml(JspServlet)trueJSP开发模式开关
checkIntervalweb.xml(JspServlet)0JSP后台检查间隔(秒)
modificationTestIntervalweb.xml(JspServlet)4JSP修改检查间隔(秒)
maxLoadedJspsweb.xml(JspServlet)-1最大加载JSP数量
org.apache.jasper.runtime.BodyContentImpl.BUFFER_SIZEcontext.xml8192JSP输出缓冲区大小
org.apache.catalina.STRICT_SERVLET_COMPLIANCEcontext.xmlfalse是否严格遵循Servlet规范

缓存命中率目标参考值:

  • 页面级缓存:>70%
  • 数据级缓存:>85%
  • 片段级缓存:>60%

当缓存命中率低于上述阈值时,建议重新评估缓存策略和实现方式。

【免费下载链接】tomcat Tomcat是一个开源的Web服务器,主要用于部署Java Web应用程序。它的特点是易用性高、稳定性好、兼容性广等。适用于Java Web应用程序部署场景。 【免费下载链接】tomcat 项目地址: https://gitcode.com/gh_mirrors/tom/tomcat

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值