1. 为什么我们需要精准清理缓存?
上周隔壁工位老张的遭遇让我记忆犹新:电商大促期间商品价格调整后,用户看到的还是旧价格。运维同学直接清空了全部缓存,导致瞬间数据库查询量暴增,整个系统差点挂掉。这种"宁可错杀一千"的清理方式,在如今精细化运营的时代显然已经不适用了。
精准清理缓存就像给缓存系统做"微创手术",既能切除病灶又不会伤及无辜。想象一下这些场景:
- 修改了某商品的库存后立即生效
- 用户更新头像后实时展示新图片
- 突发新闻事件需要快速刷新页面数据
- API接口返回数据格式调整后全网生效
这些都需要我们掌握定向清除的"手术刀级"操作,接下来就让我们解锁OpenResty中的各种"秘密武器"。
2. OpenResty缓存体系速览
在开始实战之前,先了解OpenResty的缓存生态(配简要架构图脑补):
+---------------+
| Lua Land |
+-------+-------+
|
+-------+-------+
| Shared Dict |
+-------+-------+
|
+-------+-------+
| LRU Cache |
+-------+-------+
|
+-------+-------+
| Redis/Memcache|
+---------------+
这里重点讲三种典型缓存形态:
- 共享字典(shared_dict):多Worker共享的K-V存储
- Lua模块级缓存:使用
ngx.shared
或第三方库管理 - 外部缓存服务:通过Redis等中间件实现
今天我们的手术刀主要针对前两种本地缓存,但原理同样适用于外部缓存。
3. 精准清理五式详解
(每个招式约500字+完整代码示例)
3.1 直捣黄龙式 - 直接删除法
location /clear-cache {
content_by_lua_block {
local cache = ngx.shared.my_cache
-- 获取待删除的缓存键
local key_to_delete = ngx.var.arg_key
-- 执行删除操作(返回删除数量)
local success, err = cache:delete(key_to_delete)
if not success then
ngx.log(ngx.ERR, "删除失败:", err)
ngx.exit(500)
end
ngx.say("成功删除键:", key_to_delete)
}
}
应用场景:已知完整缓存键时的即时清理
优势:简单直接、实时生效
注意点:需要精确掌握键生成规则
3.2 横扫千军式 - 模式匹配清理
location /batch-clear {
content_by_lua_block {
local cache = ngx.shared.my_cache
local pattern = ngx.var.arg_pattern or ""
-- 获取所有键列表
local keys = cache:get_keys()
-- 遍历匹配的键
local count = 0
for _, key in ipairs(keys) do
if string.find(key, pattern) then
cache:delete(key)
count = count + 1
end
end
ngx.say("批量删除完成,共清理", count, "个键")
}
}
典型场景:清理某个用户/品类的所有相关缓存
隐患:遍历操作可能影响性能(建议控制遍历频率)
3.3 瞒天过海式 - 版本号控制法
location /api/data {
content_by_lua_block {
local cache = ngx.shared.my_cache
local base_key = "user_profile_"
local version = cache:get("data_version") or 1
-- 生成带版本号的缓存键
local final_key = base_key .. ngx.var.arg_uid .. "_v" .. version
-- 获取缓存或查询数据库
local data = cache:get(final_key)
if not data then
data = query_db()
cache:set(final_key, data)
end
ngx.say(data)
}
}
-- 版本更新接口
location /update-version {
content_by_lua_block {
ngx.shared.my_cache:incr("data_version", 1)
ngx.say("版本已更新至:", ngx.shared.my_cache:get("data_version"))
}
}
精妙之处:无需删除旧缓存,通过版本迭代自然淘汰
最佳实践:配合TTL使用实现自动清理
4. 进阶技巧与避坑指南
4.1 缓存雪崩预防方案
当批量清理后突然涌入大量请求时:
-- 使用互斥锁控制重建过程
local lock_key = "rebuild_lock:" .. key
local lock_ttl = 5 -- 秒
if not cache:get(key) then
local lock = cache:add(lock_key, true, lock_ttl)
if lock then
-- 获取数据库数据
local data = query_db()
cache:set(key, data)
cache:delete(lock_key)
else
-- 等待其他线程完成重建
ngx.sleep(0.5)
return cache:get(key)
end
end
4.2 分布式环境下的协同作战
当存在多台OpenResty节点时,推荐使用Redis发布订阅:
-- 清理指令发布端
local redis = require "resty.redis"
local red = redis:new()
red:publish("cache_clean_channel", "user_123")
-- 订阅端
local handler = function(msg)
if msg == "user_123" then
ngx.shared.my_cache:delete(msg)
end
end
red:subscribe("cache_clean_channel", handler)
5. 技术选型终极PK台
(对比表格呈现)
清理方式 | 响应速度 | 内存消耗 | 实现复杂度 | 适用场景 |
---|---|---|---|---|
直接删除 | ⚡️⚡️⚡️⚡️⚡️ | ⚡️⚡️ | ⚡️ | 精确单键清理 |
批量模式匹配 | ⚡️⚡️ | ⚡️⚡️⚡️ | ⚡️⚡️ | 模糊条件清理 |
版本号控制 | ⚡️⚡️⚡️ | ⚡️⚡️⚡️ | ⚡️⚡️⚡️ | 数据结构变更 |
外部缓存联动 | ⚡️⚡️⚡️ | ⚡️ | ⚡️⚡️⚡️⚡️ | 分布式系统 |
TTL过期策略 | ⚡️ | ⚡️⚡️⚡️ | ⚡️ | 时效性要求不高的数据 |
6. 从代码到生产的必经之路
在实施缓存清理方案时,请牢记三条军规:
- 键名规范:建立明确的命名规范(如
service:type:id:version
) - 操作审计:记录所有缓存变更日志
- 防御编程:所有删除操作添加权限验证
一个血泪教训:某同学在预发环境测试时,误将ngx.shared.*
写成ngx.share.*
,结果触发了未定义的变量导致Worker崩溃。所以请务必:
-- 正确的安全写法
local ok, cache = pcall(ngx.shared.my_cache)
if not ok then
ngx.log(ngx.ERR, "缓存初始化失败")
return
end
7. 总结与展望
缓存管理就像走钢丝,需要在性能与准确性之间找到平衡。本文介绍的方案已经过多个千万级项目的验证,但技术永远在演进:比如OpenResty 1.21版本新增的ngx.shared.DICT.flush_expired()
方法,可以更优雅地处理过期键;再如与Kong网关的结合使用,可以打造更智能的缓存策略。
最后送大家一句缓存管理的箴言:"知其然更要知其所以然,方能庖丁解牛游刃有余"。下次当你举起缓存清理的"手术刀"时,希望已经胸有成竹。