从nginx源码看协商缓存

前言

协商缓存,八股文必背知识点相信大家看过不少文章了。但是,很多时候知识点只是单纯的讲出逻辑,这样不仅难记,而且容易在传播的过程中逐渐被带偏了。下面,我将从nginx源码的角度重新拆解协商缓存的机制。

起因

一般文章讲协商缓存都会讲Etag和Last-Modified两个方案。简单地说etag是根据文件内容生成的一个标识,有些文章会说是通过对内容hash生成的标识,类似md5,判断标识是否变化来决定是否返回资源文件。last-modified是文件的上次修改时间,判断这个时间的变化来决定是否返回资源文件。

一般情况下,服务器都是默认两者都同时使用,那么第一个问题就是哪个会是优先级更高的。大部分文章都说etag优先级更高,我一开始也觉得没问题。因为判断标识很明显是更加准确的,文件变了那就不应该用缓存了,而修改时间很有可能在极短的时间内多次修改内容而导致时间没有变。逻辑都说得通直到我看到了一篇文章这么说:

image.png

别的文章都是提一下etag优先级高,我第一次看到讲具体流程的,而且非常反直觉。作为内容hash标识的etag都相同的情况下,服务器怎么可能继续去判断last-modified呢,我以为的优先级高是有etag就不对last-modified做判断啊。

这时候我感觉到,这么细节的问题应该很难通过搜索引擎得到答案,再一想,nginx很轻量的啊,能不能直接看源码找到答案?

查阅nginx源码

搜索思路就不细说了,我是在项目里查相关的关键词,比如304If-None-Match 这些header字段等等。

下面看看关键源码

nginx/src/http/modules/ngx_http_not_modified_filter_module.c

image.png

前面的两个if就是了,结果是不是很意外。很明显是优先判断last-modified,并且他的思路是一旦判断为false,就return出去了,这个return是返回资源。只有当两个判断都为true,才返回304。

分析

至此虽然有了答案,但是我们没有搞明白为什么nginx要这样做。我们迷惑的原因在于我们一直是以用户的角度去思考。如果我们公司的产品让我们去实现一个nginx的协商缓存机制,就是要实现这两种方案,我们有两种思路,四种组合(伪代码):

/** 思路一 **/

// 组合一
if(header_in.if_modified_since && checkTime(header_in.if_modified_since)) return response(304)
if(header_in.if_none_match && checkEtag(header_in.if_none_match)) return response(304)
return newResource()

// 组合二
if(header_in.if_none_match && checkEtag(header_in.if_none_match)) return response(304)
if(header_in.if_modified_since && checkTime(header_in.if_modified_since)) return response(304)
return newResource()

/** 思路二 **/

// 组合三
if(header_in.if_modified_since && !checkTime(header_in.if_modified_since)) return newResource()
if(header_in.if_none_match && !checkEtag(header_in.if_none_match)) return newResource()
return response(304)

// 组合三
if(header_in.if_none_match && !checkEtag(header_in.if_none_match)) return newResource()
if(header_in.if_modified_since && !checkTime(header_in.if_modified_since)) return newResource()
return response(304)
复制代码

首先组合一肯定是不可取的,因为如果判断修改时间先,修改时间精度才到秒,一旦出现1秒内多次改动文件,就会出现判断失误而没有返回新文件。

其次我们考虑组合二,如果把etag判断放前面:如果etag匹配,返回304没问题;如果etag不匹配,这个时候我们判断修改时间同样也是有问题的,etag都不匹配了,如果这个修改时间相同,返回304,功能上还是不对。

思路一是不可行了,然后看思路二。

这里两个判断无论前后,其实都不影响功能,都是合理的,任意一个对不上就应该返回新文件了。那我们接下来就要考虑实现的效率问题了。etag这里只用了一个checkEtag方法表示判断,但我们仔细想想,判断etag是不是需要即时把服务器上的文件计算一个etag出来再和header传过来的etag做对比?计算文件的etag,这个操作可不简单啊,因为如果文件大,那消耗必然很高。判断修改时间只是判断一个现成字符串,明显消耗很小。

结论

那么方案就出来了,优先判断修改时间,不匹配则返回新文件,避免计算文件etag消耗,修改时间匹配则继续判断etag,都匹配则返回304。这就是nginx的方案了。

番外

其实看源码同时我也有去测试我的本地的nginx表现,当我想办法去控制变量时发现,nginx的etag计算方法居然非常粗暴,仅仅是计算文件内容长度,而且还把修改时间塞进去了 方法ngx_http_set_etag,我看到时蒙了,原来根本没法控制变量。我尝试用脚本在1秒内多次修改文件并且不改变位数,发现还真的会出现误判。至此我深刻体会到一个道理,实践出真知,大家共勉。

注意:etag计算方式不同服务器不一定相同,我看apache好像就是计算hash的,有兴趣可以深入研究一下。
复制代码

Guess you like

Origin juejin.im/post/7062314789984272421