-- /etc/nginx/lua/two_level_cache.lualocal lrucache = require "resty.lrucache"local lock = require "resty.lock"-- L1:每个 Worker 独立的进程内 LRU 缓存-- 创建在模块级别(只在 Worker 启动时创建一次,后续所有请求复用同一实例)local L1_CACHE_SIZE = 1000 -- 最多缓存 1000 个 keylocal l1_cache, err = lrucache.new(L1_CACHE_SIZE)if not l1_cache then error("failed to create L1 cache: " .. (err or "unknown"))endlocal L1_TTL = 5 -- L1 缓存 5 秒(短 TTL,保证一定的新鲜度)local L2_TTL = 60 -- L2 缓存 60 秒(长 TTL,避免频繁穿透)local _M = {}-- 从后端获取数据的函数(需要调用方提供)-- loader 是一个 function(key) → value, err 的函数function _M.get(key, loader) -- Step 1:查 L1 local val = l1_cache:get(key) if val ~= nil then return val -- L1 命中,直接返回(零锁、零拷贝) end -- Step 2:查 L2 local shared_cache = ngx.shared.my_cache val = shared_cache:get(key) if val ~= nil then -- L2 命中:反序列化,写入 L1,返回 local decoded = require("cjson").decode(val) l1_cache:set(key, decoded, L1_TTL) return decoded end -- Step 3:L1 L2 都未命中,需要从后端加载 -- 使用分布式锁防止多个协程同时穿透后端(防击穿) local lock_obj = lock:new("lock_cache", { exptime = 10, -- 锁最长持有 10 秒(防死锁) timeout = 5, -- 等待锁的最长时间 5 秒 }) local elapsed, err = lock_obj:lock("loader:" .. key) if not elapsed then -- 获取锁失败(超时),降级:直接调用 loader 不加锁 ngx.log(ngx.WARN, "failed to acquire cache lock: ", err) return loader(key) end -- 获得锁后,再次检查 L2(可能另一个协程刚刚写入) val = shared_cache:get(key) if val ~= nil then lock_obj:unlock() local decoded = require("cjson").decode(val) l1_cache:set(key, decoded, L1_TTL) return decoded end -- 真正需要从后端加载 local data, load_err = loader(key) if not data then lock_obj:unlock() return nil, load_err end -- 写入 L2 和 L1 local encoded = require("cjson").encode(data) local ok, err, forcible = shared_cache:set(key, encoded, L2_TTL) if not ok then ngx.log(ngx.WARN, "failed to write L2 cache: ", err) end l1_cache:set(key, data, L1_TTL) lock_obj:unlock() return dataendreturn _M
使用示例:
location /api/user { content_by_lua_block { local cache = require "two_level_cache" local user_id = ngx.var.arg_id local user, err = cache.get("user:" .. user_id, function(key) -- 从 Redis 加载用户数据 local redis = require "resty.redis" local red = redis:new() red:connect("127.0.0.1", 6379) local data = red:hgetall("user:" .. user_id) red:set_keepalive(60000, 100) return data end) if not user then ngx.status = 404 ngx.say('{"error":"user not found"}') return end ngx.header["Content-Type"] = "application/json" ngx.say(require("cjson").encode(user)) }}
-- 错误做法(每次请求都编译正则)content_by_lua_block { local uri = ngx.var.request_uri -- 每次请求执行到这里时,ngx.re.match 会编译正则 "^/api/v([0-9]+)/" -- 编译正则需要 PCRE 库调用,约耗时 10-50 微秒 -- 1000 QPS = 每秒 10-50ms 纯花在正则编译上 local m, err = ngx.re.match(uri, "^/api/v([0-9]+)/") if m then local version = m[1] -- ... end}
-- 危险:在请求处理中做大量计算(如复杂的加密运算、大文件哈希)content_by_lua_block { local data = ngx.req.get_body_data() -- SHA-256 大文件哈希是 CPU 密集的,可能占用几十毫秒 local hash = compute_sha256(data) -- 阻塞事件循环!}
对于 CPU 密集型任务,应使用 ngx.thread.spawn 启动轻量级线程,或将计算 offload 到后端服务。
-- Lua 惯用法:返回 nil + error_stringlocal function do_something() local ok, err = some_operation() if not ok then return nil, "operation failed: " .. err end return resultend-- 调用方处理local val, err = do_something()if not val then ngx.log(ngx.ERR, "error: ", err) return ngx.exit(500)end
机制二:pcall 保护异常
-- 对于可能 panic 的操作(如解码畸形 JSON),使用 pcall 保护local ok, result = pcall(cjson.decode, json_str)if not ok then ngx.log(ngx.ERR, "JSON decode error: ", result) ngx.exit(400) returnend
-- /etc/nginx/lua/dynamic_router.lualocal cache = require "two_level_cache"local cjson = require "cjson.safe"local _M = {}function _M.route() local user_id = ngx.ctx.user_id -- 由 jwt_auth 插件设置 local uri = ngx.var.uri -- 从缓存获取路由规则 local rules, err = cache.get("routing_rules", function(key) local redis = require "resty.redis" local red = redis:new() red:connect("127.0.0.1", 6379) local data = red:get("routing_rules") red:set_keepalive(60000, 100) if data and data ~= ngx.null then return cjson.decode(data) end return {} end) if not rules then ngx.var.upstream_name = "api_v1" -- 降级到 v1 return end -- 灰度逻辑:按用户 ID 哈希选择 canary if rules.canary_enabled then local user_hash = ngx.crc32_short(user_id or "anonymous") % 100 if user_hash < (rules.canary_percentage or 5) then ngx.var.upstream_name = "api_canary" ngx.log(ngx.INFO, "user ", user_id, " routed to canary") return end end -- 按 API 版本路由 local m = ngx.re.match(uri, "^/api/v([12])/", "jo") if m then ngx.var.upstream_name = "api_v" .. m[1] else ngx.var.upstream_name = "api_v1" endendreturn _M