一、缓存穿透:当查询变成"无底洞"

想象一下这个场景:你的电商平台每秒要处理10万次商品查询请求,其中有个恶意用户一直在用根本不存在的商品ID发起请求。由于缓存里没有这个数据,每次请求都直接打到数据库,数据库很快就会被拖垮。这就是典型的缓存穿透问题。

在OpenResty中,我们可以用Lua脚本来实现布隆过滤器。下面是一个完整的实现示例(技术栈:OpenResty + Lua):

-- 初始化布隆过滤器
local bloom_filter = require "resty.bloomfilter"
local bf, err = bloom_filter.new(1000000, 0.001)  -- 容量100万,错误率0.1%

-- 预热数据(实际应该从数据库加载)
local items = {"product_1001", "product_1002", "product_1003"}
for _, item in ipairs(items) do
    bf:add(item)
end

-- 在access阶段检查
local function check_product_id()
    local product_id = ngx.var.arg_product_id
    if not product_id then
        ngx.exit(ngx.HTTP_BAD_REQUEST)
    end
    
    -- 先检查布隆过滤器
    if not bf:check(product_id) then
        ngx.log(ngx.INFO, "Blocked non-existent product request: ", product_id)
        ngx.exit(ngx.HTTP_NOT_FOUND)
    end
end

这个方案有几个关键点需要注意:

  1. 布隆过滤器需要预热真实存在的键
  2. 错误率设置需要权衡内存和准确性
  3. 对于新增数据需要及时更新过滤器

二、缓存雪崩:当缓存集体"罢工"

缓存雪崩比穿透更可怕,它就像是缓存系统突然集体罢工。比如你的缓存设置了相同的过期时间,结果在同一时刻全部失效,导致所有请求直接涌向数据库。

在OpenResty中,我们可以采用多级缓存+随机过期时间的策略。看这个实现示例:

-- 多级缓存实现
local redis = require "resty.redis"
local shared_cache = ngx.shared.product_cache  -- 这是nginx共享内存

local function get_product(product_id)
    -- 第一层:本地共享内存
    local product = shared_cache:get(product_id)
    if product then
        return product
    end
    
    -- 第二层:Redis缓存
    local red = redis:new()
    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.log(ngx.ERR, "Redis connect failed: ", err)
        -- 继续尝试数据库查询
    else
        product = red:get(product_id)
        if product and product ~= ngx.null then
            -- 设置随机过期时间(基础300秒 + 0-300秒随机值)
            local ttl = 300 + math.random(300)
            shared_cache:set(product_id, product, ttl)
            return product
        end
    end
    
    -- 第三层:数据库查询
    product = query_db(product_id)
    if product then
        -- 同时写入Redis和共享内存
        local ttl = 300 + math.random(300)
        red:set(product_id, product)
        shared_cache:set(product_id, product, ttl)
    end
    
    return product
end

这个方案的精髓在于:

  1. 通过多级缓存分散压力
  2. 随机过期时间避免同时失效
  3. 即使某层缓存失效,系统仍可降级运行

三、热点Key问题:当某个商品突然爆红

双十一期间,某款手机突然爆红,每秒被查询10万次。即使有缓存,单个Key的极高并发也可能打爆缓存服务器。

OpenResty的Lua协程机制可以很好地解决这个问题:

local resty_lock = require "resty.lock"

local function get_hot_product(product_id)
    local cache = ngx.shared.product_cache
    local product = cache:get(product_id)
    
    if product then
        return product
    end
    
    -- 使用分布式锁防止缓存击穿
    local lock = resty_lock:new("product_locks", {timeout=0.1, exptime=1})
    local elapsed, err = lock:lock(product_id)
    
    if not elapsed then
        -- 获取锁失败,直接返回旧数据或默认值
        local stale = cache:get_stale(product_id)
        return stale or {default=true}
    end
    
    -- 临界区:只有一个请求能执行这里
    product = query_db(product_id)
    if product then
        cache:set(product_id, product, 60)  -- 正常缓存
    else
        cache:set(product_id, {default=true}, 5)  -- 空值缓存
    end
    
    lock:unlock()
    return product
end

这个方案的关键技术点:

  1. 使用轻量级锁避免并发重建缓存
  2. 支持返回陈旧数据保证可用性
  3. 对空结果也进行短期缓存

四、缓存更新策略:保证数据一致性

缓存最难的不是技术实现,而是保证数据一致性。我们来看看几种常见策略在OpenResty中的实现。

1. 主动更新策略示例:

-- 商品更新接口
local function update_product()
    local args = ngx.req.get_post_args()
    local product_id = args.product_id
    
    -- 先更新数据库
    local ok = update_db(product_id, args)
    if not ok then
        ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    end
    
    -- 再删除缓存
    local red = redis:new()
    red:connect("127.0.0.1", 6379)
    red:del(product_id)
    
    ngx.shared.product_cache:delete(product_id)
    
    -- 异步更新其他节点缓存
    ngx.timer.at(0, function()
        notify_other_nodes(product_id)
    end)
end

2. 延迟双删策略示例:

local function update_product_with_double_delete()
    -- 第一次删除
    delete_cache(product_id)
    
    -- 更新数据库
    update_db(product_id, data)
    
    -- 延迟第二次删除
    ngx.timer.at(1.0, function()
        delete_cache(product_id)
    end)
end

缓存更新的黄金法则:

  1. 先更数据库再删缓存
  2. 对于重要数据采用双删策略
  3. 考虑引入消息队列保证可靠性

五、实战经验与避坑指南

在实际项目中,我总结了这些血泪教训:

  1. 监控指标必须完善

    • 缓存命中率要分层次监控
    • 慢查询需要设置阈值告警
    • 内存使用率要实时关注
  2. 测试时的注意事项

    -- 压力测试时模拟缓存失效
    local function mock_cache_failure()
        if ngx.var.arg_test_mode == "cache_failure" then
            ngx.shared.product_cache:flush_all()
            local red = redis:new()
            red:connect("127.0.0.1", 6379)
            red:flushall()
        end
    end
    
  3. 灰度发布策略

    • 新缓存策略要先在小流量验证
    • 支持快速回滚机制
    • 要有降级开关
  4. Key设计规范

    • 业务前缀:比如 "product:1001"
    • 版本号:"v1:product:1001"
    • 避免特殊字符

六、未来演进方向

随着业务发展,缓存架构也需要不断进化:

  1. 混合缓存策略

    • 本地缓存 + 分布式缓存 + 数据库
    • 热点数据自动识别
  2. 智能缓存预热

    -- 基于历史数据预测预热
    local function smart_preheat()
        local hot_items = predict_hot_items()
        for _, item in ipairs(hot_items) do
            preload_to_cache(item)
        end
    end
    
  3. 弹性缓存架构

    • 根据负载自动调整缓存大小
    • 动态TTL策略

缓存系统就像系统的免疫系统,需要持续优化和调整。希望这些实战经验能帮你少走弯路!