实现 cache-aside 并测命中率
目标
亲手实现 cache-aside,用数字确认未命中与命中的成本差异,并进一步加上负缓存和版本键失效。
为什么重要
cache-aside 是最常见的模式,所以代码在哪里都能复制到。真正难的是细节:写入时是更新缓存还是删除缓存,不存在的数据要不要缓存,以及需要一次性让全部数据失效时该怎么办。第一点——答案是删除而不是更新,原因在于两次写入交错时,更新可能留下旧值,而删除不会。第二点——反复查询不存在的键的流量,会穿过缓存,原样打在源数据上。这叫缓存穿透,用短 TTL 的负缓存来防范。第三点——商品类目整体变化时,把相关的键全部找出来删掉,会掉进追踪依赖关系的地狱。版本键把它变成了只需把一个版本数字加一。
步骤
- 在 127.0.0.1:8151 上启动
/opt/app/slowdb.py。GET /item/1返回 200,GET /stats返回{"queries":<n>}。 - 在
/root/ca/cache.py中编写get_item(id)。缓存未命中时,回源查询,并以 300 秒存入item:<id>键。在/root/ca/miss.out中写入source=origin ms=<정수>(占位符为整数),ms 必须大于等于 100。 - 再次查询同一个条目。在
/root/ca/hit.out中写入source=cache ms=<정수> origin_queries_delta=0(占位符为整数),ms 必须小于 50。 /root/ca/update.py在更新源数据之后,删除缓存键。源码中不能出现把更新后的值重新写进缓存的代码。运行之后EXISTS item:1必须是 0。- 查询不存在的
item:9999时,源数据返回 404,并把这个结果以 60 秒的生存时间缓存起来。第二次查询时回源查询次数不能增加,且TTL item:9999必须大于等于 1 且小于等于 60。 - 用
/root/ca/bench.py执行 100 次查询(20 个不同的键),并在/root/ca/hitratio.txt中写入hits=<n> misses=<n> ratio=<소수>(占位符依次为命中次数、未命中次数与小数)。ratio 必须大于等于 0.75。 - 把缓存键改成
v<버전>:item:<id>(占位符依次为版本号与 id)的形式,并用ver:item键管理版本。提升版本后再查询,回源查询次数必须增加。在/root/ca/version.out中写入old_ver=<n> new_ver=<n> refetched=true。
参考
- 查看回源查询次数:
curl -s http://127.0.0.1:8151/stats - 负缓存的 TTL 要设得比正常缓存短得多——因为数据很快就可能出现。
- 常见错误 1:写入时更新缓存——并发写入时可能留下旧值。
- 常见错误 2:没有做负缓存,导致带着不存在的 ID 来请求的流量原样打在源数据上(缓存穿透)。
启动慢速源数据服务
在 127.0.0.1:8151 上启动 /opt/app/slowdb.py。GET /item/1 返回 200,GET /stats 返回 {"queries":<n>}。
/opt/app/slowdb.py 每次查询都会故意耗费时间。它还会告知自己收到了多少次查询。
构造缓存未命中的路径
在 /root/ca/cache.py 中编写 get_item(id)。缓存未命中时,回源查询,并以 300 秒存入 item:<id> 键。在 /root/ca/miss.out 中写入 source=origin ms=<정수>(占位符为整数),ms 必须大于等于 100。
缓存里没有,就去源端走一趟,放进缓存再返回。第一次查询必然是慢的。
让第二次查询由缓存处理
再次查询同一个条目。在 /root/ca/hit.out 中写入 source=cache ms=<정수> origin_queries_delta=0(占位符为整数),ms 必须小于 50。
再次查询同一个键时,不应再回源。确认回源查询次数没有增加。
更新时删除缓存
/root/ca/update.py 在更新源数据之后,删除缓存键。源码中不能出现把更新后的值重新写进缓存的代码。运行之后 EXISTS item:1 必须是 0。
是删除,而不是更新。原因在阅读资料中。下一次查询会自行补上最新的值。
对不存在的键做负缓存
查询不存在的 item:9999 时,源数据返回 404,并把这个结果以 60 秒的生存时间缓存起来。第二次查询时回源查询次数不能增加,且 TTL item:9999 必须大于等于 1 且小于等于 60。
反复查询不存在的键,每次都会回源。用较短的生存时间,把“不存在”也缓存起来。
测量命中率
用 /root/ca/bench.py 执行 100 次查询(20 个不同的键),并在 /root/ca/hitratio.txt 中写入 hits=<n> misses=<n> ratio=<소수>(占位符依次为命中次数、未命中次数与小数)。ratio 必须大于等于 0.75。
分别统计命中和未命中,得出比例。数值会随请求分布而不同。
用版本键让整体失效
把缓存键改成 v<버전>:item:<id>(占位符依次为版本号与 id)的形式,并用 ver:item 键管理版本。提升版本后再查询,回源查询次数必须增加。在 /root/ca/version.out 中写入 old_ver=<n> new_ver=<n> refetched=true。
把版本埋进键名,只要提升版本,就再也没有人去找旧键。不必亲自去删除。