1. 为什么要给动态内容"穿衣服"?

想象你在快餐店点单,每次都要现做汉堡肯定要等很久。但如果把做好的汉堡放在保温柜里,服务员直接取给顾客就快多了。动态内容缓存就是给服务器准备的"保温柜",把频繁请求的动态计算结果暂时存起来,下次直接取用。

在OpenResty体系中,我们使用Nginx+LuaJIT的技术组合,配合其丰富的缓存模块,可以实现既保留动态生成灵活性,又享受静态内容响应速度的效果。这套组合拳尤其适合电商秒杀、实时榜单更新这类"既要又要"的场景。

2. 两种典型缓存方案对比

2.1 内存缓存(lua-resty-lrucache)

就像随身携带的备忘录,适合快速存取但容量有限:

-- 初始化LRU缓存(容量1000条,生命周期60秒)
local lrucache = require "resty.lrucache"
local cache, err = lrucache.new(1000)
if not cache then
    ngx.log(ngx.ERR, "创建缓存失败:", err)
end

-- 获取当前请求路径作为缓存key
local key = ngx.var.request_uri

-- 先尝试读取缓存
local value = cache:get(key)
if value then
    ngx.say("来自缓存的响应:", value)
    return
end

-- 缓存未命中时执行业务逻辑
local result = expensive_calculation() -- 模拟耗时计算
cache:set(key, result, 0.6) -- 缓存60秒
ngx.say("新鲜出炉的响应:", result)

技术栈:OpenResty 1.21 + lua-resty-lrucache 0.13

优点

  • 零网络延迟,内存直接存取
  • 配置简单,像使用本地字典
  • 自动淘汰旧数据防止内存溢出

缺点

  • 单Worker进程独享,不同Worker缓存不共享
  • 容量受限不能存储大型数据
  • 重启服务缓存即丢失

2.2 分布式缓存(lua-resty-redis)

类似小区里的快递柜,多个配送员都能存取:

-- 连接Redis集群(生产环境建议配置连接池)
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000) -- 1秒超时

-- 使用请求参数生成唯一key
local args = ngx.req.get_uri_args()
local key = "cache:"..ngx.md5(table.concat(args, ":"))

-- 先查缓存
local ok, err = red:connect("redis-cluster", 6379)
if not ok then
    ngx.log(ngx.ERR, "Redis连接失败:", err)
    return direct_response() -- 降级处理
end

local cached = red:get(key)
if cached and cached ~= ngx.null then
    red:set_keepalive(10000, 100) -- 归还连接池
    ngx.say("集群缓存命中:", cached)
    return
end

-- 执行实际业务逻辑
local fresh_data = generate_content(args)
-- 写入缓存并设置随机过期时间防止雪崩
math.randomseed(ngx.now())
red:set(key, fresh_data, "EX", 300 + math.random(60))
red:set_keepalive(10000, 100)
ngx.say("实时生成数据:", fresh_data)

技术栈:OpenResty 1.21 + lua-resty-redis 0.30 + Redis 6.2

优点

  • 多节点共享缓存
  • 支持更大数据量存储
  • 持久化保证重启不丢失

缺点

  • 增加网络通信开销
  • 需要额外维护缓存集群
  • 数据一致性维护成本高

3. 给缓存穿上"防弹衣"的注意事项

3.1 缓存雪崩预防

某电商在零点促销时,大量缓存同时过期导致数据库瞬间被打垮。解决方案是给每个缓存项设置随机过期时间:

local ttl = 3600 + math.random(600) -- 基础1小时+随机10分钟
cache:set(key, value, ttl)

3.2 缓存击穿应对

使用互斥锁防止热点数据失效时被重复查询:

local lock_key = key..":lock"
if cache:get(key) == nil then
    if cache:add(lock_key, true, 2) then -- 获取2秒锁
        local data = query_db()
        cache:set(key, data)
        cache:delete(lock_key)
    else
        while cache:get(lock_key) do
            ngx.sleep(0.1) -- 短暂等待
        end
        return cache:get(key)
    end
end

3.3 缓存穿透防护

对不存在的数据请求设置空值标记:

if data == nil then
    cache:set(key, "NIL_FLAG", 60) -- 特殊标记缓存
    return nil
end

4. 适合穿"缓存外套"的场景

  • 商品详情页的规格参数(变更频率低)
  • 用户地理位置信息(短时稳定)
  • 排行榜数据(允许分钟级延迟)
  • 接口鉴权token(有效期内不变)

5. 不该穿这件"外套"的时候

  • 金融交易流水(要求绝对实时)
  • 高频变更的库存数量
  • 需要强一致性的订单状态
  • 敏感的用户隐私数据

6. 实践中的经验总结

在给某视频网站优化弹幕服务时,使用两级缓存策略:先查本地LRU缓存,未命中再查Redis集群,最终QPS从800提升到23000,服务器资源消耗降低60%。但同时也遇到过因缓存时间设置过长导致用户看到过期推荐列表的问题,最终通过组合使用主动刷新和被动失效机制解决。

缓存就像做菜时的预制菜,用得好能大幅提升出餐速度,但需要掌握好"保鲜期"。OpenResty提供的缓存工具就像智能冰箱,既能冷藏保存,又能及时提醒食材过期。关键是理解业务场景的真实需求,在数据新鲜度和响应速度之间找到最佳平衡点。