写给后端程序员的HTTP缓存原理

本文介绍如何通过HTTP缓存策略优化网页性能,包括使用ETag验证令牌、定义Cache-Control指令和更新缓存响应的方法。

通过网络获取内容既速度缓慢又开销巨大。较大的响应需要在客户端与服务器之间进行多次往返通信,这会延迟浏览器获得和处理内容的时间,还会增加访问者的流量费用。因此,缓存并重复利用之前获取的资源的能力成为性能优化的一个关键方面。

好在每个浏览器都自带了 HTTP 缓存实现功能。您只需要确保每个服务器响应都提供正确的 HTTP 标头指令,以指示浏览器何时可以缓存响应以及可以缓存多久。

注:如果您在应用中使用 Webview 来获取和显示网页内容,可能需要提供额外的配置标志,以确保 HTTP 缓存得到启用、其大小根据用例进行了合理设置并且缓存将持久保存。务必查看平台文档并确认您的设置!

HTTP 请求

当服务器返回响应时,还会发出一组 HTTP 标头,用于描述响应的内容类型、长度、缓存指令、验证令牌等。例如,在上图的交互中,服务器返回一个 1024 字节的响应,指示客户端将其缓存最多 120 秒,并提供一个验证令牌(“x234dff”),可在响应过期后用来检查资源是否被修改。

通过 ETag 验证缓存的响应

TL;DR

  • 服务器使用 ETag HTTP 标头传递验证令牌。
  • 验证令牌可实现高效的资源更新检查:资源未发生变化时不会传送任何数据。

假定在首次获取资源 120 秒后,浏览器又对该资源发起了新的请求。首先,浏览器会检查本地缓存并找到之前的响应。遗憾的是,该响应现已过期,浏览器无法使用。此时,浏览器可以直接发出新的请求并获取新的完整响应。不过,这样做效率较低,因为如果资源未发生变化,那么下载与缓存中已有的完全相同的信息就毫无道理可言!

这正是验证令牌(在 ETag 标头中指定)旨在解决的问题。服务器生成并返回的随机令牌通常是文件内容的哈希值或某个其他指纹。客户端不需要了解指纹是如何生成的,只需在下一次请求时将其发送至服务器。如果指纹仍然相同,则表示资源未发生变化,您就可以跳过下载。

HTTP Cache-Control 示例

在上例中,客户端自动在“If-None-Match” HTTP 请求标头内提供 ETag 令牌。服务器根据当前资源核对令牌。如果它未发生变化,服务器将返回“304 Not Modified”响应,告知浏览器缓存中的响应未发生变化,可以再延用 120 秒。请注意,您不必再次下载响应,这节约了时间和带宽。

作为网络开发者,您如何利用高效的重新验证?浏览器会替我们完成所有工作:它会自动检测之前是否指定了验证令牌,它会将验证令牌追加到发出的请求上,并且它会根据从服务器接收的响应在必要时更新缓存时间戳。我们唯一要做的就是确保服务器提供必要的 ETag 令牌。检查您的服务器文档中有无必要的配置标志。

注:提示:HTML5 Boilerplate 项目包含所有最流行服务器的配置文件样例,其中为每个配置标志和设置都提供了详细的注解。在列表中找到您喜爱的服务器,查找合适的设置,然后复制/确认您的服务器配置了推荐的设置。

Cache-Control

TL;DR

  • 每个资源都可通过 Cache-Control HTTP 标头定义其缓存策略
  • Cache-Control 指令控制谁在什么条件下可以缓存响应以及可以缓存多久。

从性能优化的角度来说,最佳请求是无需与服务器通信的请求:您可以通过响应的本地副本消除所有网络延迟,以及避免数据传送的流量费用。为实现此目的,HTTP 规范允许服务器返回 Cache-Control 指令,这些指令控制浏览器和其他中间缓存如何缓存各个响应以及缓存多久。

注:Cache-Control 标头是在 HTTP/1.1 规范中定义的,取代了之前用来定义响应缓存策略的标头(例如 Expires)。所有现代浏览器都支持 Cache-Control,因此,使用它就够了。

HTTP Cache-Control 示例

“no-cache”和“no-store”

“no-cache”表示必须先与服务器确认返回的响应是否发生了变化,然后才能使用该响应来满足后续对同一网址的请求。因此,如果存在合适的验证令牌 (ETag),no-cache 会发起往返通信来验证缓存的响应,但如果资源未发生变化,则可避免下载。

相比之下,“no-store”则要简单得多。它直接禁止浏览器以及所有中间缓存存储任何版本的返回响应,例如,包含个人隐私数据或银行业务数据的响应。每次用户请求该资产时,都会向服务器发送请求,并下载完整的响应。

“public”与“private”

如果响应被标记为“public”,则即使它有关联的 HTTP 身份验证,甚至响应状态代码通常无法缓存,也可以缓存响应。大多数情况下,“public”不是必需的,因为明确的缓存信息(例如“max-age”)已表示响应是可以缓存的。

相比之下,浏览器可以缓存“private”响应。不过,这些响应通常只为单个用户缓存,因此不允许任何中间缓存对其进行缓存。例如,用户的浏览器可以缓存包含用户私人信息的 HTML 网页,但 CDN 却不能缓存。

“max-age”

指令指定从请求的时间开始,允许获取的响应被重用的最长时间(单位:秒)。例如,“max-age=60”表示可在接下来的 60 秒缓存和重用响应。

定义最佳 Cache-Control 策略

缓存决策树

按照以上决策树为您的应用使用的特定资源或一组资源确定最佳缓存策略。在理想的情况下,您的目标应该是在客户端上缓存尽可能多的响应,缓存尽可能长的时间,并且为每个响应提供验证令牌,以实现高效的重新验证。

Cache-Control 指令和说明
max-age=86400浏览器以及任何中间缓存均可将响应(如果是“public”响应)缓存长达 1 天(60 秒 x 60 分钟 x 24 小时)。
private, max-age=600客户端的浏览器只能将响应缓存最长 10 分钟(60 秒 x 10 分钟)。
no-store不允许缓存响应,每次请求都必须完整获取。

根据 HTTP Archive,在排名最高的 300,000 个网站(按照 Alexa 排名)中,所有下载的响应中几乎有半数可由浏览器缓存,这可以大量减少重复的网页浏览和访问。当然,这并不意味着您的特定应用有 50% 的资源可以缓存。一些网站的资源 90% 以上都可以缓存,而其他网站可能有许多私密或时效要求高的数据根本无法缓存。

请审核您的网页,确定哪些资源可以缓存,并确保它们返回正确的 Cache-Control 和 ETag 标头。

废弃和更新缓存的响应

TL;DR

  • 在资源“过期”之前,将一直使用本地缓存的响应。
  • 您可以通过在网址中嵌入文件内容指纹,强制客户端更新到新版本的响应。
  • 为获得最佳性能,每个应用都需要定义自己的缓存层次结构。

浏览器发出的所有 HTTP 请求会首先路由到浏览器缓存,以确认是否缓存了可用于满足请求的有效响应。如果有匹配的响应,则从缓存中读取响应,这样就避免了网络延迟和传送产生的流量费用。

不过,如果您想更新或废弃缓存的响应,该怎么办?例如,假定您已告诉访问者将某个 CSS 样式表缓存长达 24 小时 (max-age=86400),但设计人员刚刚提交了一个您希望所有用户都能使用的更新。您该如何通知拥有现在“已过时”的 CSS 缓存副本的所有访问者更新其缓存?在不更改资源网址的情况下,您做不到。

浏览器缓存响应后,缓存的版本将一直使用到过期(由 max-age 或 expires 决定),或一直使用到由于某种其他原因从缓存中删除,例如用户清除了浏览器缓存。因此,构建网页时,不同的用户可能最终使用的是文件的不同版本;刚获取了资源的用户将使用新版本的响应,而缓存了早期(但仍有效)副本的用户将使用旧版本的响应。

所以,如何才能鱼和熊掌兼得:客户端缓存和快速更新?您可以在资源内容发生变化时更改它的网址,强制用户下载新响应。通常情况下,可以通过在文件名中嵌入文件的指纹或版本号来实现 - 例如 style.x234dff.css。

缓存层次结构

因为能够定义每个资源的缓存策略,所以您可以定义“缓存层次结构”,这样不但可以控制每个响应的缓存时间,还可以控制访问者看到新版本的速度。为了进行说明,我们一起分析一下上面的示例:

  • HTML 被标记为“no-cache”,这意味着浏览器在每次请求时都始终会重新验证文档,并在内容变化时获取最新版本。此外,在 HTML 标记内,您在 CSS 和 JavaScript 资产的网址中嵌入指纹:如果这些文件的内容发生变化,网页的 HTML 也会随之改变,并会下载 HTML 响应的新副本。
  • 允许浏览器和中间缓存(例如 CDN)缓存 CSS,并将 CSS 设置为 1 年后到期。请注意,您可以放心地使用 1 年的“远期过期”,因为您在文件名中嵌入了文件的指纹:CSS 更新时网址也会随之变化。
  • JavaScript 同样设置为 1 年后到期,但标记为 private,这或许是因为它包含的某些用户私人数据是 CDN 不应缓存的。
  • 图像缓存时不包含版本或唯一指纹,并设置为 1 天后到期。

您可以组合使用 ETag、Cache-Control 和唯一网址来实现一举多得:较长的过期时间、控制可以缓存响应的位置以及随需更新。

缓存检查清单

不存在什么最佳缓存策略。您需要根据通信模式、提供的数据类型以及应用特定的数据更新要求,为每个资源定义和配置合适的设置,以及整体的“缓存层次结构”。

在制定缓存策略时,您需要牢记下面这些技巧和方法:

  • 使用一致的网址:如果您在不同的网址上提供相同的内容,将会多次获取和存储这些内容。提示:请注意,网址区分大小写
  • 确保服务器提供验证令牌 (ETag):有了验证令牌,当服务器上的资源未发生变化时,就不需要传送相同的字节。
  • 确定中间缓存可以缓存哪些资源:对所有用户的响应完全相同的资源非常适合由 CDN 以及其他中间缓存进行缓存。
  • 为每个资源确定最佳缓存周期:不同的资源可能有不同的更新要求。为每个资源审核并确定合适的 max-age。
  • 确定最适合您的网站的缓存层次结构:您可以通过为 HTML 文档组合使用包含内容指纹的资源网址和短时间或 no-cache 周期,来控制客户端获取更新的速度。

  • 最大限度减少搅动:某些资源的更新比其他资源频繁。如果资源的特定部分(例如 JavaScript 函数或 CSS 样式集)会经常更新,可以考虑将其代码作为单独的文件提供。这样一来,每次获取更新时,其余内容(例如变化不是很频繁的内容库代码)可以从缓存获取,从而最大限度减少下载的内容大小。

Reference:

https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn

<think>我们正在准备后端开发面试,需要整理常见面试问题及其参考答案。根据引用[1]和引用[2],面试官会考察分布式系统、并发、数据结构与算法、数据库、网络、系统设计等方面的问题。同时,引用[3]提到了可变值与可变值的选择,引用[4]提到了线程的等待和唤醒机制,这些都是并发编程中的重要知识点。 下面我们将分类列出后端开发面试的常见问题,并提供简要的参考答案。注意,由于篇幅限制,参考答案会过于详细,但会覆盖核心要点。 ### 一、数据结构与算法 1. **快速排序的原理是什么?** 快速排序是一种分治算法。它选择一个元素作为基准(pivot),将数组分为两部分:小于基准的元素和大于等于基准的元素,然后递归地对这两部分进行排序。 时间复杂度:平均$O(n \log n)$,最坏$O(n^2)$(当数组已经有序时)。优化方法包括随机选择基准或使用三数取中法。 2. **如何实现二叉树的遍历?** - 前序遍历:根节点 -> 左子树 -> 右子树 - 中序遍历:左子树 -> 根节点 -> 右子树 - 后序遍历:左子树 -> 右子树 -> 根节点 可以使用递归或迭代(栈)实现。 ### 二、数据库 1. **数据库事务的ACID特性是什么?** - 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚。 - 一致性(Consistency):事务执行前后,数据库状态必须保持一致(满足所有约束)。 - 隔离性(Isolation):多个事务并发执行时,一个事务的执行应影响其他事务。 - 持久性(Durability):一旦事务提交,其结果就是永久性的。 2. **什么是SQL注入?如何防止?** SQL注入是攻击者通过输入恶意SQL语句来操纵数据库。防止方法: - 使用预编译语句(Prepared Statements)和参数化查询。 - 对用户输入进行过滤和转义。 - 最小化数据库权限。 ### 三、网络 1. **TCP和UDP的区别?** - TCP是面向连接的、可靠的、基于字节流的传输层协议,提供超时重传、流量控制、拥塞控制等机制,保证数据顺序和正确性。 - UDP是无连接的、可靠的、基于数据报的协议,保证顺序和正确性,但开销小、速度快。 2. **HTTP状态码有哪些?** - 1xx:信息响应 - 2xx:成功(如200 OK) - 3xx:重定向(如301 永久重定向) - 4xx:客户端错误(如404 未找到) - 5xx:服务器错误(如500 内部服务器错误) ### 四、并发编程 1. **线程和进程的区别?** - 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。 - 一个进程可以包含多个线程,线程共享进程的内存空间,而进程之间内存独立。 - 线程切换开销小于进程。 2. **wait()和sleep()的区别?** - `wait()`是Object类的方法,调用时会释放锁,需要被`notify()`或`notifyAll()`唤醒。 - `sleep()`是Thread类的方法,调用时释放锁,时间到后自动恢复运行[^4]。 ### 五、分布式系统 1. **分布式系统中常见的问题和挑战是什么?如何解决?** - 问题:网络延迟、分区、节点故障、数据一致性等。 - 解决方案: - 使用超时和重试处理网络问题。 - 通过副本机制(Replication)提高可用性。 - 使用分布式一致性算法(如Raft、Paxos)保证数据一致性。 - 采用分布式事务协议(如两阶段提交)[^1]。 ### 六、系统设计 1. **如何设计一个短链接服务?** - 将长链接映射为短链接(使用哈希或自增ID)。 - 使用分布式键值存储(如Redis)缓存映射关系。 - 使用数据库持久化存储。 - 考虑高并发和扩展性。 ### 七、编程语言(以Python为例) 1. **Python中可变对象和可变对象有什么区别?** - 可变对象:创建后能修改(如int, tuple, str)。修改操作会创建新对象。 - 可变对象:创建后可以修改(如list, dict, set)。 - 使用可变对象可以提高并发安全性和可预测性[^3]。 2. **Python的GIL是什么?** GIL(全局解释器锁)是CPython解释器中的互斥锁,它确保同一时刻只有一个线程执行Python字节码。这导致多线程程序在CPU密集型任务中无法充分利用多核。 以上是后端开发面试中常见的问题及参考答案。建议根据自身知识短板进行深入学习,并多做实践。 §§相关问题§§ 1. 如何优化数据库查询性能? 2. 解释CAP定理及其在分布式系统中的应用。 3. 什么是RESTful API?设计原则是什么? 4. 解释HTTP/2的新特性。 5. 如何设计一个高并发的秒杀系统?</think>### 后端开发面试常见问题及参考答案 #### 一、数据结构与算法 1. **快速排序的原理与实现** 快速排序采用分治策略: - 选择基准元素(pivot) - 将数组分为小于基准和大于基准的两部分 - 递归排序子数组 时间复杂度:平均 $O(n \log n)$,最坏 $O(n^2)$ 优化:随机选择基准或三数取中法避免最坏情况 ```python def quicksort(arr): if len(arr) <= 1: return arr pivot = arr[len(arr)//2] left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quicksort(left) + middle + quicksort(right) ``` 2. **二叉树遍历的实现** - 前序遍历:根 → 左 → 右 - 中序遍历:左 → 根 → 右 - 后序遍历:左 → 右 → 根 递归实现示例(中序): ```python def inorder(root): return inorder(root.left) + [root.val] + inorder(root.right) if root else [] ``` --- #### 二、数据库 1. **事务的ACID特性** - **原子性**(Atomicity):事务全部成功或全部回滚 - **一致性**(Consistency):事务前后数据满足完整性约束 - **隔离性**(Isolation):并发事务互干扰 - **持久性**(Durability):提交后数据永久存储 实现方式:WAL(Write-Ahead Logging)和锁机制[^1] 2. **SQL索引优化** - 索引类型:B+树(范围查询)、哈希(等值查询) - 最左前缀原则:联合索引 $(a,b,c)$ 仅支持 $a$、$a,b$、$a,b,c$ 查询 - 避免索引失效:使用函数转换字段、避免 `OR` 条件 --- #### 三、并发编程 1. **线程同步机制** - `synchronized`(Java)/ `Lock`(Python):保证代码块互斥访问 - `wait()` 与 `notify()`:线程等待与唤醒(需在同步块中使用)[^4] - 信号量(Semaphore):控制并发线程数 2. **可变性与可变性** - **可变对象**(如String):线程安全,无需锁 - **可变对象**(如HashMap):需同步控制 优先使用可变对象提高并发安全性[^3] --- #### 四、分布式系统 1. **CAP定理** - **一致性**(Consistency):所有节点数据一致 - **可用性**(Availability):每个请求都能响应 - **分区容错性**(Partition tolerance):网络分区时系统仍可用 分布式系统必须满足 **P**,在 **C** 和 **A** 间权衡(如CP的ZooKeeper vs AP的Cassandra)[^1] 2. **分布式事务解决方案** - **两阶段提交(2PC)**:协调者统一决策,存在阻塞风险 - **TCC模式**(Try-Confirm-Cancel):业务层补偿机制 - **消息队列**:最终一致性(如RabbitMQ事务消息) --- #### 五、网络协议 1. **TCP vs UDP** | 特性 | TCP | UDP | |------------|----------------|--------------| | 连接 | 面向连接 | 无连接 | | 可靠性 | 可靠(重传机制) | 可靠 | | 速度 | 慢 | 快 | | 应用场景 | HTTP/MySQL | 视频流/DNS | 2. **HTTP状态码** - `200 OK`:请求成功 - `404 Not Found`:资源存在 - `503 Service Unavailable`:服务可用 - `401 Unauthorized`:未认证 --- #### 六、系统设计 1. **短链接系统设计** - 哈希算法:将长URL映射为62进制短码(如 `aBc1d`) - 存储:Redis缓存键值对,MySQL持久化 - 容错:布隆过滤器防恶意请求 2. **高并发限流策略** - **令牌桶算法**:恒定速率生成令牌,突发流量可处理 - **漏桶算法**:固定速率处理请求 - 工具:Redis + Lua脚本实现分布式限流 --- ### 面试准备建议 1. **基础知识**:重点复习算法、数据库、操作系统 2. **项目经验**:用STAR法则(情境-任务-行动-结果)描述项目 3. **系统设计**:从需求分析 → 数据模型 → 接口设计逐步展开 4. **行为问题**:准备“团队冲突处理”“技术决策依据”等案例
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值