VODK 项目中的多层缓存架构实践
视频聚合站 tv.odkkk.com 是怎么把多个不稳定的数据源变成一个顺滑的站点的——聊聊背后的多层缓存思路。
这个网站是做什么的
tv.odkkk.com 是我做的一个小站,简单说就是一个视频聚合入口。它自己不存片源,而是把网上好几个资源站的内容攒到一起,让用户不用再来回跳站找片子。
它主要做三件事:
- 一次搜全网:输入一个片名,后台同时去多个资源站问一遍,把结果合并、去重、排好序再返回。用户看到的是一张干净的列表,而不是在五六个站之间反复粘贴片名。
- 补全信息:资源站返回的数据通常很糙,常常只有标题和播放地址。我会拿这些去豆瓣对一下评分、简介、海报,番剧再补一下 Bangumi 的信息,让每条结果都像样。
- 记住你看到哪了:播放进度、搜索历史、收藏夹都跟账号走,换页面也不会丢。
听起来不复杂,但真正做起来才发现,难受的不是功能本身,而是数据来源太不靠谱。资源站动不动超时、被拦,豆瓣翻多了就 403,可首页推荐又不能一直显示上周的内容。如果每次点开都直接去源头拉数据,体验会很糟——有时秒开,有时转圈半天,有时直接报错。
所以这个站能不能用,关键不在前端长什么样,而在数据层能不能把上游的”抽风”藏起来,让用户永远感觉是顺的。这就是后面这套缓存架构要解决的问题。
核心思路:让数据离用户越近越好
整个数据层借用了 CPU 缓存那套思路——从最快的到最慢的,一层一层兜底:
请求 → 边缘缓存 → 热数据缓存 → 持久存储 → 上游源站
每一层只在 自己没命中 时才往下问,命中就直接返回。这样既把响应压到最快,又把对上游的压力降到最低。下面一层一层说。
第一层:边缘缓存
最外面是 CDN 边缘节点。同一个请求,只要最近有人问过,边缘节点就直接把上次的响应原样回给你,根本不到我的服务器。这就好比楼下小卖部囤了畅销货,不用每次都去总仓调。
这里有个小技巧:让过期的响应也先返回,同时在后台悄悄更新。用户拿到的是”上一秒的旧数据”,但下一秒就刷新好了——体感上永远是秒开,不用干等回源。
第二层:热数据缓存
边缘缓存没命中,请求才到应用层。这里有个内存级的快缓存,专门放最近被频繁访问的数据。
它最聪明的地方在于会自己判断热度:一个数据被访问得越多,它的留存时间就越长。刚进来的冷数据只留一分钟,但如果它开始被人反复点,就自动续到几分钟、十分钟、半小时。反过来,没人看的内容很快就过期让位,不占地方。
结果是:越火的内容越常驻,冷门内容自然淘汰。不用人为去猜”哪些该留”,访问量自己会投票。
另外有个细节:写数据时我会顺手再存一份”兜底副本”,有效期是正本的五倍。万一正本刚好过期、正在回源,用户至少能先拿到兜底的旧数据,而不是对着加载圈发呆。
第三层:持久存储
热缓存的数据过期后并不会真的消失——它会从一层持久存储里”回温”过来。这层是长期保存的,相当于一份离线快照:就算上游站今天彻底挂了,我这边还有上次抓到的数据能顶上,用户顶多看到稍微旧一点的内容,而不是一个错误页。
这里我做了两件小事来避免无意义的折腾:
一是只有内容真变了才覆盖。每次写入前先算个”指纹”比对一下,内容没变就只更新一下访问时间和计数,不重写数据本身。这样那些被反复访问但内容其实没变的热门片,不会天天在数据库里做无用的写入。
二是该新的数据得让它新。持久层是永久保留的,但有些东西不能一直喂旧数据——比如首页推荐,老显示一周前的榜单就没意思了。所以给每类数据设了个保鲜期,比如首页推荐一天必须刷新一次,搜索结果可以存得久一点。过了保鲜期,就算持久层有,也强制重新去源头拉一份。
第四层:上游源站
真的走到这一步,说明前面三层全没命中,这才是真正去敲资源站大门的时候。拉回来的新数据,我会顺手同时塞进热缓存和持久层——而且是不等它写完就先回应用户。下次同样的请求,直接从缓存就命中了。
写库不挡路
往持久层写数据这一步,最容易拖慢响应。所以我把写入做成了”事后异步”:先把数据返回给用户,再在后台慢慢落库。就算写库失败了也不影响这次访问,顶多下次请求多回一次源。
把”用户感觉快不快”和”数据有没有存牢”拆开来看,这个取舍很值——数据库慢一点用户无感,但响应慢半秒用户立刻就烦了。
效果
这套架构跑下来,体感上最直观的两点:
一是快。首页基本秒开,搜索也基本在百毫秒内。绝大多数请求根本碰不到上游。
二是稳。某个资源站今天抽风了,用户毫无感知——因为要么热缓存里有,要么持久层里有,只是数据稍微旧一点点。等它恢复了,下次抓取自然就更新了。
写在最后
做完这个项目,我对”缓存”这件事的看法变了不少。以前觉得缓存就是”把结果存起来下次快一点”,现在觉得它更像是在替用户消化上游的不确定性——快只是副产品,稳才是真正解决的问题。
好的缓存策略不是让所有数据都永久存着,而是让热数据常驻、冷数据能回温、过期数据会刷新。