HTTP篇—如何利用HTTP缓存提高响应速率

什么是HTTP缓存?

HTTP 缓存是一种用于提高网页加载速度、减少网络流量降低服务器负载的技术。它允许客户端(浏览器)或中间代理(CDN)存储网页资源的副本,比如浏览器将缓存数据存储在用户计算机的本地磁盘中,而CDN将缓存数据存储在 CDN 服务提供商管理的、离用户最近的CDN服务器当中。这样后续请求就能直接从这些缓存中获取资源,而无需再从后端数据库检索数据

图1和图2分别来自CSDN的头像请求(注意要关闭"缓存禁用"选项),图1使用内存缓存,因为是短时间的连续请求,图2使用磁盘缓存,因为是隔了相对较长的时间发起的请求
内存缓存
磁盘缓存

请求头和响应头中哪些字段与HTTP缓存相关?

  • 请求头:
    • Cache-Control:客户端用来告知服务器或缓存代理它希望如何控制缓存行为(no-cacheno-storemax-age=0)。
    • If-Modified-Since / If-None-Match:客户端用来询问服务器资源是否有更新,如果资源未修改,服务器返回 304 状态码,客户端则使用本地缓存版本。
      • **If-Modified-Since:**上一次接收到资源时服务器返回的 Last-Modified
      • If-None-Match:上一次接收到资源时服务器返回的 ETag
  • 响应头:
    • Cache-Control:服务器设置规则,以确定该用户是否将从其本地缓存加载资源(no-cacheno-storemax-age=3600)。
    • Expires:指定客户端缓存过期的时间。
    • Last-Modified:表示资源的最后修改时间,在下一次请求时,客户端可以用 If-Modified-Since 来判断缓存是否有效。
    • ETag:资源的唯一标识符,基于内容生成的哈希值。让客户端使用 If-None-Match 与资源当前状态做比较,以判断缓存是否有效。

Etag和Last-Modified的区别

  • Last-Modified: 精度有限,只能精确到秒。这可能导致:

    • 一秒内多次修改的文件无法精确标记。
    • 内容未变但定期保存的文件,Last-Modified 也会更新,导致缓存失效。
  • ETag 通过资源内容的哈希值来标识,解决了 Last-Modified 的精度问题。然而,计算哈希值比获取修改时间消耗更多服务器资源。

  • ETagLast-Modified 协同工作:

    服务器会优先验证 ETag。如果 ETag 匹配,服务器才会进一步检查 Last-Modified,防止一些 HTTP 服务器未将文件修改日期纳入哈希范围内。

HTTP缓存类别

  • 强制缓存:缓存有效期间不需要请求后端服务器,由客户端自身进行控制,比如设置HTTP响应头中的max-age=3600
  • 协商缓存:缓存有效期需要与后端服务器进行交互才能判断,比如后端服务器保存Last-ModifiedEtag,与客户端请求头的If-Modified-SinceIf-None-Match进行比较,如果相同则返回304,客户端会使用HTTP缓存响应数据,否则经过

协商缓存相比服务端Redis缓存或其本地缓存的优势是?

  • 协商缓存将缓存数据保存在用户计算机的磁盘当中,减轻了服务端的内存压力并节省了带宽。

如何应用HTTP缓存?

  • 应用场景:HTTP缓存一般用于静态资源传输(html、css、js)

  • 强制缓存使用:对于文件数据,如果采用minio分布式存储或云OSS服务来存储文件,允许设置HTTP响应头,设置max-age值进行强制缓存

    public class MinioCacheExample {
    
        public static void main(String[] args) {
            String minioUrl = "http://localhost:9000"; // 你的 MinIO 服务器地址
            String accessKey = "minioadmin"; // 你的 MinIO access key
            String secretKey = "minioadmin"; // 你的 MinIO secret key
            String bucketName = "my-test-bucket"; // 你的桶名称
            String objectName = "my-cached-file.txt"; // 要上传的对象名称
            String filePath = "/path/to/your/local/file.txt"; // 本地文件路径
    
            try {
                // 创建 MinIO 客户端
                MinioClient minioClient = MinioClient.builder()
                        .endpoint(minioUrl)
                        .credentials(accessKey, secretKey)
                        .build();
    
                // 检查桶是否存在,如果不存在则创建
                boolean found = minioClient.bucketExists(io.minio.BucketExistsArgs.builder().bucket(bucketName).build());
                if (!found) {
                    minioClient.makeBucket(io.minio.MakeBucketArgs.builder().bucket(bucketName).build());
                    System.out.println("桶 '" + bucketName + "' 已创建。");
                } else {
                    System.out.println("桶 '" + bucketName + "' 已存在。");
                }
    
                // 设置自定义 HTTP 头用于缓存
                Map<String, String> headers = new HashMap<>();
    
                // Cache-Control: 设置缓存策略
                // public: 允许任何缓存机制缓存响应
                // max-age=<seconds>: 缓存的最大秒数。这里设置为1小时(3600秒)
                // immutable: 指示资源内容在一段时间内不会改变,有助于浏览器更积极地缓存
                // other common options: no-cache, no-store, must-revalidate, proxy-revalidate
                long maxAgeSeconds = TimeUnit.HOURS.toSeconds(1); // 缓存 1 小时
                headers.put("Cache-Control", "public, max-age=" + maxAgeSeconds + ", immutable");
    
                // 上传文件并设置自定义头
                try (InputStream is = new FileInputStream(filePath)) {
                    minioClient.putObject(
                            PutObjectArgs.builder()
                                    .bucket(bucketName)
                                    .object(objectName)
                                    .stream(is, -1, 10485760) // -1 表示未知大小,10MB 的分块大小
                                    .headers(headers) // 设置自定义 HTTP 头
                                    .build());
                    System.out.println("文件 '" + objectName + "' 已成功上传到桶 '" + bucketName + "',并设置了缓存头。");
                }
    
            } catch (Exception e) {
                System.err.println("发生错误: " + e.getMessage());
                e.printStackTrace();
            }
        }
    }
    
  • 协商缓存使用:对于可能被修改的数据,需要采用协商缓存,在数据被修改时,及时提醒客户端缓存数据已经失效。此时通过Last-ModifiedEtag来控制

    流程:检查查询接口(包括参数)请求头的If-Modified-Since值与后端保存的Last-Modified比较,如果相同则返回304状态码,否则进行正常的查询并更新响应头Last-Modified字段的值。在调用修改接口时更新服务端保存的Last-Modified为当前时间。

    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    
    public class CacheInterceptor implements HandlerInterceptor {
    
        // 使用 ConcurrentMap 来保证并发安全
        // 存储 URI -> 最后修改时间(RFC 1123 格式)
        private static final ConcurrentMap<String, String> LAST_MODIFIED_MAP = new ConcurrentHashMap<>();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String uri = request.getRequestURI();
    
            // --- 处理 /notice/selectAll 接口的缓存 ---
            if ("/notice/selectAll".equals(uri)) {
                // 获取客户端发送的 If-Modified-Since 头
                String clientIfModifiedSince = request.getHeader("If-Modified-Since");
    
                // 从 Map 中获取该资源最新的 Last-Modified 时间
                // 注意:首次启动应用或首次请求时,Map 中可能没有这个值,需要初始化
                String latestLastModified = LAST_MODIFIED_MAP.computeIfAbsent(uri, k -> getLatestNoticeModifiedTime());
    
                // 比较客户端缓存时间和服务器最新修改时间
                if (clientIfModifiedSince != null && clientIfModifiedSince.equals(latestLastModified)) {
                    // 如果客户端缓存是最新的,则返回 304 Not Modified
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                    return false; // 不继续处理请求
                } else {
                    // 如果客户端缓存不是最新的,或者没有缓存,则发送新的内容和 Last-Modified 头
                    response.addHeader("Last-Modified", latestLastModified);
                    // max-age=3600 表示缓存 1 小时
                    // public 表示可以被任何缓存机制缓存
                    // must-revalidate 表示过期后必须重新验证
                    response.addHeader("Cache-Control", "public, max-age=3600, must-revalidate");
                }
            }
    
            if ("/notice/add".equals(uri)) {
                updateNoticeLastModified();
            }
    
            return true;
        }
        
        private String getLatestNoticeModifiedTime() {
            // 模拟从数据库或其他数据源获取最新通知的修改时间
            // 示例:每当这个方法被调用时,返回一个稍微不同的时间,模拟数据更新
            return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now());
        }
    
        // 在实际的 NoticeService 中更新这个时间
        public static void updateNoticeLastModified() {
            // 当有通知被添加、更新或删除时,调用此方法更新 /notice/selectAll 的最新修改时间
            LAST_MODIFIED_MAP.put("/notice/selectAll", DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now()));
            System.out.println("通知数据已更新,'/notice/selectAll' 的 Last-Modified 时间已刷新。");
        }
        
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

艾露z

谢谢侬!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

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

抵扣说明:

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

余额充值