“缓存”在计算机技术中被大量使用,比如从计算机体系结构中存储层次结构到Web系统中的web缓存、反向代理缓存以及浏览器缓存等。缓存主要用来解决费时操作的重复计算,主要是把费时操作的计算结果保存至磁盘、内存等介质中,下一个请求到来时直接将结果返回,这就避免了重复的操作,节省了cpu、带宽等资源。当然,万物都是有利弊的,缓存的缺点就是不能保证时效性,雪崩效应等。
对于高性能的Web应用,缓存是至关重要,这里涉及到的技术包括:页面缓存、动态内容缓存、opcode缓存、浏览器缓存、web缓存以及反向代理缓存等。本文介绍一下最贴近用户的浏览器缓存,后续会逐步介绍其他技术。
Web缓存是介于web服务器和浏览器中间的一层,用于缓存web服务器的http响应,而浏览器本身也是带有缓存功能的。比如,IE、FireFox或者Chrome会把浏览器http响应的内容缓存在硬盘或者内存中。而如何使浏览器去使用缓存这个是依赖于HTTP协议的,我们知道浏览器与web服务器通信是通过HTTP协议的,所以在web服务器端的程序需要通过HTTP协议来控制浏览器去使用本地的缓存。下面所有的例子采用的都是python的轻量级的web框架web.py。
首先写一个server端程序cache_tst.py,用于返回当前时间:
import web
import datetime
urls = (
'/index', 'index'
)
class index:
def GET(self):
return datetime.datetime.now()
app = web.application(urls, globals())
if __name__ == '__main__':
app.run()
这个程序很简单,监听8080端口,只会处理url为/index的请求,然后返回当前时间。通过浏览器访问以下localhost:8080/index,结果显示:
2012-03-13 16:12:58.729000
响应结果就是当前的时间。下面通过FireFox或者Chrome的js工作台查看一下http请求和响应。
响应头中只有Date、Server和Transfer-Encoding三个字段,没有与缓存相关的,下面看如何实现缓存协商。
1. Last-Modified和If-Modified-Since
我们知道缓存是有失效时间的,所以浏览器必须知道当前的HTTP响应的时间戳,这样才能用这个时间戳去判断该响应是否失效。这就需要用到HTTP协议的Last-Modified属性,web服务器通过设置Last-Modified属性可以让浏览器知道这个HTTP响应的时间戳。而浏览器知道这个时间戳之后,当再次请求时会在请求头中添加If-Modified-Since属性。
比如,修改cache_tst.py如下:
import web
import datetime
urls = ('/index', 'index')
class index:
def GET(self):
# web.lastmodified用于设置Last-Modified属性
web.lastmodified(datetime.datetime.now())
return datetime.datetime.now()
app = web.application(urls, globals())
if __name__ == '__main__':
app.run()
再访问localhost:8080/index,我们看一下HTTP请求和响应。
import web
import datetime
urls = (
'/index', 'index'
)
def cache_valid(cache_time):
'''
cache_time: 缓存时间
根据http协议,判断缓存是否失效。如果缓存失效则设置Last-Modified属性,
否则返回Http响应状态码304 Not Modified
'''
last_time_str = web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '')
last_time = web.net.parsehttpdate(last_time_str)
now = datetime.datetime.now()
if last_time and last_time + datetime.timedelta(seconds = cache_time) > now:
web.notmodified()
return True
else:
web.lastmodified(now)
return False
class index:
cache_time = 30
def GET(self):
if not cache_valid(self.cache_time):
return datetime.datetime.now()
app = web.application(urls, globals())
if __name__ == '__main__':
app.run()
然后请求两次localhost:8080/index,http的状态码变为304:
2. Expires
缓存失效判断这一操作完全可以由浏览器自身实现,没必要由服务器完成,这样每次建立、释放连接以及网络开销很耗费资源,下面我们来彻底消灭这种请求。这里需要用到Expires属性,标记http响应的失效的时间。修改cache_tst.py:
import web
import datetime
urls = (
'/index', 'index'
)
class index:
cache_time = 30
def GET(self):
now = datetime.datetime.now()
web.http.expires(datetime.timedelta(seconds = self.cache_time, days = 1))# 设置Expires属性,传入timedelta对象
return now
app = web.application(urls, globals())
if __name__ == '__main__':
app.run()
参看一下请求头和响应头:
提示响应内容是从cache中读取的,也就是说这一次访问没有经过http连接,是在本地由浏览器完成的缓存失效检验。但是,这里有个问题,Expires返回的是精确的时间,如果浏览器和web服务器的时区不同,那么这个过期时间是没有用的,下面看一下Cache-Control是如何解决这个问题的。
3. Cache-Control
Cache-Control通过max-age=xxx指定缓存时间,然后具体的失效时间由浏览器计算得到,这样就不存在时区不同导致的错误,下面试验一下。
import web
import datetime
urls = (
'/index', 'index'
)
class index:
cache_time = 30
def GET(self):
now = datetime.datetime.now()
web.header('Cache-Control', 'max-age=30')
return now
app = web.application(urls, globals())
if __name__ == '__main__':
app.run()
请求和响应信息:
4. 缓存失效时浏览器的行为
Firefox 3.5 | IE 8 | Chrome 3 | Safari 4 | |
---|---|---|---|---|
内容没有失效 | 浏览器呈现来自缓存的页面 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器呈现来自缓存的页面 | 浏览器呈现来自缓存的页面 |
内容失效 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器重新发送请求到服务器。返回代码是 200 |
| Firefox 3.5 | IE 8 | Chrome 3 | Safari 4 |
---|---|---|---|---|
内容没有失效 | 浏览器呈现来自缓存的页面 | 浏览器呈现来自缓存的页面 | 浏览器重新发送请求到服务器。返回代码是 304 | 浏览器重新发送请求到服务器。返回代码是 304 |
内容失效 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器呈现来自缓存的页面 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器重新发送请求到服务器。返回代码是 200 |
Firefox 3.5 | IE 8 | Chrome 3 | Safari 4 | |
---|---|---|---|---|
内容没有失效 | 浏览器重新发送请求到服务器。返回代码是 304 | 浏览器重新发送请求到服务器。返回代码是 304 | 浏览器重新发送请求到服务器。返回代码是 304 | 浏览器重新发送请求到服务器。返回代码是 304 |
内容失效 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器重新发送请求到服务器。返回代码是 200 | 浏览器重新发送请求到服务器。返回代码是 200 |
Firefox 3.5 | IE 8 | Chrome 3 | Safari 4 | |
---|---|---|---|---|
内容没有失效 | 浏览器呈现来自缓存的页面 | 浏览器呈现来自缓存的页面 | 浏览器呈现来自缓存的页面 | 浏览器呈现来自缓存的页面 |
内容失效 | 浏览器呈现来自缓存的页面 | 浏览器呈现来自缓存的页面 | 浏览器呈现来自缓存的页面 | 浏览器重新发送请求到服务器。返回代码是 200 |
import web
import datetime
urls = (
'/index', 'index'
)
def cache_valid(cache_time):
'''
cache_time: 缓存时间
根据http协议,判断缓存是否失效。如果缓存失效则设置Last-Modified属性,
否则返回Http响应状态码304 Not Modified
'''
last_time_str = web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '')
last_time = web.net.parsehttpdate(last_time_str)
now = datetime.datetime.now()
if last_time and last_time + datetime.timedelta(seconds = cache_time) > now:
web.notmodified()
return True
else:
# 同时使用Last-Modified和Cache-Control
# 在用户新开窗口等情况下,会使用Cache-Control缓存
# 而当用户进行某些不进行缓存检测的操作(按F5刷新)时,
# 浏览器会向服务器发送请求,由于使用了Last-Modified属性
# 所以服务器端仍然可以返回304状态码,避免重复的费时计算。
web.lastmodified(now)
web.header('Cache-Control', 'max-age=30')
return False
class index:
cache_time = 30
def GET(self):
if not cache_valid(self.cache_time):
return datetime.datetime.now()
app = web.application(urls, globals())
if __name__ == '__main__':
app.run()
作为一个高性能web应用,缓存体系的构建是必不可少的,而浏览器缓存只是这个体系中的一小部分,对于一些图片,js脚本,css以及一些更新频率很低的页面(1天,1周,1月等)可以很好的发挥作用,而像其他的一些动态内容,更新频率比较高的,浏览器缓存就并不适用了。这时就是页面缓存,web缓存,分布式缓存上场的时候了,后续会分别介绍如何构建一个缓存体系。