1. Nginx与Redis联动的核心价值
在现代Web架构中,Nginx作为高性能的反向代理服务器,Redis作为内存数据库,二者的组合能够解决许多传统架构中的性能瓶颈问题。这种联动模式特别适合以下场景:
- 高并发请求下的缓存加速
- 分布式会话存储
- 实时计数器与排行榜
- 秒杀系统的库存控制
- API限流与熔断机制
我曾在电商大促期间使用这种架构处理过峰值QPS超过5万的场景,相比传统数据库直连方案,响应时间从平均200ms降低到了15ms左右,数据库负载下降了90%。
2. OpenResty与lua-resty-redis基础配置
2.1 环境准备
首先需要安装OpenResty,它集成了Nginx与LuaJIT,并内置了lua-resty-redis模块:
bash复制# Ubuntu/Debian
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main"
sudo apt-get update
sudo apt-get install -y openresty
# CentOS/RHEL
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install -y openresty
2.2 基本连接配置
在Nginx配置文件中添加以下内容:
nginx复制http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
server {
location /redis-test {
resolver 8.8.8.8;
content_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
-- 设置超时时间(连接/发送/读取)
red:set_timeouts(1000, 1000, 1000) -- 单位毫秒
-- 连接到Redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("连接失败: ", err)
return
end
-- 认证(如果Redis配置了密码)
local res, err = red:auth("yourpassword")
if not res then
ngx.say("认证失败: ", err)
return
end
-- 执行Redis命令
local res, err = red:set("dog", "an animal")
if not res then
ngx.say("设置失败: ", err)
return
end
-- 获取数据
local res, err = red:get("dog")
if not res then
ngx.say("获取失败: ", err)
return
end
ngx.say("dog的值是: ", res)
-- 放入连接池
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.say("设置keepalive失败: ", err)
return
end
}
}
}
}
3. 高级应用场景实现
3.1 缓存策略实现
在实际项目中,我们通常采用多级缓存策略。以下是典型的缓存读取逻辑:
lua复制local function get_from_cache(key)
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return nil
end
local res, err = red:get(key)
if not res then
ngx.log(ngx.ERR, "Redis获取失败: ", err)
red:close()
return nil
end
if res == ngx.null then
-- 缓存未命中
red:set_keepalive(10000, 100)
return nil
end
red:set_keepalive(10000, 100)
return res
end
local function get_from_db(key)
-- 数据库查询逻辑
-- 返回数据和TTL
return {data = "db_data", ttl = 60}
end
local function set_to_cache(key, value, ttl)
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return nil
end
local ok, err = red:setex(key, ttl, value)
if not ok then
ngx.log(ngx.ERR, "Redis设置失败: ", err)
red:close()
return nil
end
red:set_keepalive(10000, 100)
return true
end
-- 主逻辑
local key = "product_123"
local value = get_from_cache(key)
if not value then
local db_result = get_from_db(key)
if db_result then
set_to_cache(key, db_result.data, db_result.ttl)
value = db_result.data
end
end
ngx.say("最终结果: ", value)
3.2 分布式锁实现
在分布式系统中,Redis常被用来实现分布式锁:
lua复制local function acquire_lock(lock_key, lock_value, expire_time)
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return nil
end
-- 使用SET NX EX命令原子性地获取锁
local res, err = red:set(lock_key, lock_value, "NX", "EX", expire_time)
if not res then
ngx.log(ngx.ERR, "获取锁失败: ", err)
red:set_keepalive(10000, 100)
return nil
end
if res == ngx.null then
-- 锁已被其他客户端持有
red:set_keepalive(10000, 100)
return false
end
red:set_keepalive(10000, 100)
return true
end
local function release_lock(lock_key, lock_value)
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return nil
end
-- 使用Lua脚本确保只有锁的持有者才能释放锁
local script = [[
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
]]
local res, err = red:eval(script, 1, lock_key, lock_value)
if not res then
ngx.log(ngx.ERR, "释放锁失败: ", err)
red:close()
return nil
end
red:set_keepalive(10000, 100)
return res == 1
end
-- 使用示例
local lock_key = "order_lock_123"
local lock_value = ngx.worker.pid() .. ngx.now()
local expire_time = 10 -- 10秒过期
local locked = acquire_lock(lock_key, lock_value, expire_time)
if locked then
-- 执行业务逻辑
ngx.say("成功获取锁")
-- 释放锁
local released = release_lock(lock_key, lock_value)
if released then
ngx.say("成功释放锁")
else
ngx.say("释放锁失败")
end
else
ngx.say("获取锁失败")
end
4. 性能优化与最佳实践
4.1 连接池配置
正确的连接池配置对性能至关重要:
lua复制-- 推荐配置
local ok, err = red:set_keepalive(10000, 100) -- 10秒空闲时间,池大小100
-- 计算建议:
-- 假设:
-- - Redis最大连接数:1000
-- - Nginx worker数量:10
-- - 应用服务器数量:2
-- 则每个worker的连接池大小应为:1000 / 10 / 2 = 50
4.2 管道技术(Pipeline)
批量操作时使用管道可以显著减少网络往返时间:
lua复制local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return
end
-- 开启管道
red:init_pipeline()
-- 批量添加命令
red:set("user:1:name", "Alice")
red:set("user:1:age", "30")
red:set("user:1:email", "alice@example.com")
red:get("user:1:name")
-- 提交管道
local results, err = red:commit_pipeline()
if not results then
ngx.log(ngx.ERR, "管道提交失败: ", err)
red:close()
return
end
-- results是一个数组,包含每个命令的结果
ngx.say("设置结果: ", results[1]) -- 第一个SET的结果
ngx.say("获取结果: ", results[4]) -- GET的结果
red:set_keepalive(10000, 100)
4.3 Lua脚本优化
使用Lua脚本可以减少网络往返并保证原子性:
lua复制local script = [[
local key = KEYS[1]
local increment = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local current = tonumber(redis.call("get", key) or "0")
if current + increment > limit then
return {err = "超过限制"}
end
redis.call("incrby", key, increment)
redis.call("expire", key, 60) -- 60秒过期
return redis.call("get", key)
]]
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return
end
-- 执行脚本
local res, err = red:eval(script, 1, "rate_limit:user123", "1", "100")
if not res then
ngx.log(ngx.ERR, "脚本执行失败: ", err)
red:close()
return
end
if type(res) == "table" and res.err then
ngx.say("错误: ", res.err)
else
ngx.say("当前值: ", res)
end
red:set_keepalive(10000, 100)
5. 常见问题排查
5.1 连接问题排查
当遇到连接问题时,可以按照以下步骤排查:
- 检查Redis服务是否运行:
bash复制ps aux | grep redis-server
- 检查网络连通性:
bash复制telnet 127.0.0.1 6379
- 检查Redis配置:
bash复制grep -E "bind|protected-mode|requirepass" /etc/redis/redis.conf
- 检查防火墙设置:
bash复制iptables -L -n
5.2 性能问题排查
Redis性能问题通常表现为响应变慢,可以通过以下方法诊断:
- 使用Redis内置命令检查:
bash复制redis-cli info | grep -E "instantaneous_ops_per_sec|used_memory_human|connected_clients"
- 监控慢查询:
bash复制redis-cli slowlog get 10
- 检查持久化是否阻塞:
bash复制redis-cli info persistence
5.3 内存问题排查
Redis内存使用过高时:
- 查看内存使用情况:
bash复制redis-cli info memory
- 查找大key:
bash复制redis-cli --bigkeys
- 内存优化建议:
- 使用Hash代替多个String存储对象
- 设置合理的过期时间
- 考虑使用Redis的LRU淘汰策略
6. 生产环境部署建议
6.1 高可用架构
对于生产环境,建议采用以下架构:
code复制客户端 → Nginx/OpenResty → Redis Sentinel → Redis主从集群
配置示例:
lua复制local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
-- 连接到Sentinel获取主节点信息
local ok, err = red:connect("sentinel-ip", 26379)
if not ok then
ngx.log(ngx.ERR, "连接Sentinel失败: ", err)
return
end
-- 获取主节点信息
local master_info, err = red:sentinel("get-master-addr-by-name", "mymaster")
if not master_info then
ngx.log(ngx.ERR, "获取主节点失败: ", err)
red:close()
return
end
red:close()
-- 连接到主节点
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect(master_info[1], master_info[2])
if not ok then
ngx.log(ngx.ERR, "连接主节点失败: ", err)
return
end
-- 正常操作...
6.2 安全配置
- 启用Redis密码认证:
redis.conf复制requirepass yourstrongpassword
- 禁用危险命令:
redis.conf复制rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG ""
- 绑定特定IP:
redis.conf复制bind 127.0.0.1
- 启用保护模式:
redis.conf复制protected-mode yes
6.3 监控与告警
建议配置以下监控项:
- 基础监控:
- 内存使用率
- 连接数
- QPS
- 命中率
- 告警阈值:
- 内存使用 > 80%
- 连接数 > 最大连接数的80%
- 持久化失败
- 主从同步延迟 > 10秒
- 使用Prometheus + Grafana监控:
bash复制# 安装redis_exporter
wget https://github.com/oliver006/redis_exporter/releases/download/v1.15.1/redis_exporter-v1.15.1.linux-amd64.tar.gz
tar xvf redis_exporter-v1.15.1.linux-amd64.tar.gz
./redis_exporter -redis.addr redis://localhost:6379 -redis.password "yourpassword"
7. 实际案例:秒杀系统实现
7.1 架构设计
秒杀系统核心挑战在于:
- 瞬时高并发
- 库存超卖问题
- 系统稳定性
解决方案:
code复制用户 → Nginx(限流) → Lua脚本(Redis原子操作) → 数据库
7.2 核心代码实现
lua复制local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return ngx.exit(500)
end
-- 商品ID和用户ID
local product_id = ngx.var.arg_product_id or "1"
local user_id = ngx.var.arg_user_id or "1001"
-- 秒杀脚本
local script = [[
local product_key = KEYS[1] -- 商品库存key
local bought_key = KEYS[2] -- 已购买用户集合key
local user_id = ARGV[1] -- 用户ID
local expire_time = tonumber(ARGV[2]) -- 过期时间
-- 检查库存
local stock = tonumber(redis.call("get", product_key) or "0")
if stock <= 0 then
return {err = "库存不足"}
end
-- 检查是否已经购买过
local is_member = redis.call("sismember", bought_key, user_id)
if is_member == 1 then
return {err = "已经参与过秒杀"}
end
-- 扣减库存
redis.call("decr", product_key)
-- 记录购买用户
redis.call("sadd", bought_key, user_id)
redis.call("expire", bought_key, expire_time)
return {ok = "秒杀成功"}
]]
-- 执行秒杀
local res, err = red:eval(script, 2,
"seckill:stock:" .. product_id,
"seckill:users:" .. product_id,
user_id,
3600 -- 1小时过期
)
if not res then
ngx.log(ngx.ERR, "秒杀失败: ", err)
red:close()
return ngx.exit(500)
end
if type(res) == "table" and res.err then
ngx.say(res.err)
else
ngx.say("秒杀成功!")
-- 异步处理订单创建
ngx.timer.at(0, function()
local db = require "resty.mysql"
-- 数据库操作...
end)
end
red:set_keepalive(10000, 100)
7.3 压力测试
使用wrk进行压力测试:
bash复制wrk -t10 -c1000 -d30s "http://localhost/seckill?product_id=1&user_id=${RANDOM}"
优化前后对比:
- 优化前(直接访问数据库):QPS 200,失败率 80%
- 优化后(Redis+Lua):QPS 5000,失败率 0.1%
8. 扩展应用:实时排行榜
8.1 数据结构设计
使用Redis的Sorted Set实现:
code复制key: leaderboard:game:1
member: user_id
score: 积分
8.2 核心操作实现
lua复制local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return
end
-- 更新用户积分
local user_id = "user_" .. ngx.var.arg_user_id
local score = tonumber(ngx.var.arg_score) or 0
local res, err = red:zadd("leaderboard:game:1", score, user_id)
if not res then
ngx.log(ngx.ERR, "更新积分失败: ", err)
red:close()
return
end
-- 获取排行榜前10
local res, err = red:zrevrange("leaderboard:game:1", 0, 9, "WITHSCORES")
if not res then
ngx.log(ngx.ERR, "获取排行榜失败: ", err)
red:close()
return
end
-- 处理结果
ngx.say("实时排行榜:")
for i = 1, #res, 2 do
ngx.say((i+1)/2 .. ". " .. res[i] .. ": " .. res[i+1])
end
red:set_keepalive(10000, 100)
8.3 性能优化技巧
- 分段缓存:将排行榜分成多段缓存,减轻ZREVRANGE压力
- 异步更新:非实时更新可以使用异步任务批量处理
- 数据过期:设置合理的过期时间,避免数据无限增长
9. 调试与日志记录
9.1 调试技巧
- 打印完整Redis命令和响应:
lua复制local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
-- 设置日志函数
red.set_logger(function(cmd, ...)
ngx.log(ngx.INFO, "Redis命令: ", cmd, " 参数: ", ...)
end)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "连接失败: ", err)
return
end
local res, err = red:get("test_key")
if not res then
ngx.log(ngx.ERR, "操作失败: ", err)
else
ngx.log(ngx.INFO, "操作结果: ", res)
end
red:set_keepalive(10000, 100)
9.2 错误处理最佳实践
- 重试机制:
lua复制local function redis_command_with_retry(red, cmd, ...)
local retries = 3
local delay = 0.1 -- 100ms
for i = 1, retries do
local res, err = red[cmd](red, ...)
if res then
return res
end
-- 只对连接错误重试
if not err:find("connection") then
return nil, err
end
if i < retries then
ngx.sleep(delay)
delay = delay * 2 -- 指数退避
end
end
return nil, "重试次数用尽"
end
-- 使用示例
local res, err = redis_command_with_retry(red, "get", "test_key")
- 熔断机制:
lua复制local circuit_breaker = {
state = "closed", -- closed, open, half-open
failure_count = 0,
last_failure_time = 0,
reset_timeout = 60 -- 60秒后尝试恢复
}
local function redis_command_with_circuit_breaker(red, cmd, ...)
if circuit_breaker.state == "open" then
if ngx.now() - circuit_breaker.last_failure_time > circuit_breaker.reset_timeout then
circuit_breaker.state = "half-open"
else
return nil, "熔断器开启"
end
end
local res, err = red[cmd](red, ...)
if res then
if circuit_breaker.state == "half-open" then
circuit_breaker.state = "closed"
circuit_breaker.failure_count = 0
end
return res
else
circuit_breaker.failure_count = circuit_breaker.failure_count + 1
circuit_breaker.last_failure_time = ngx.now()
if circuit_breaker.failure_count >= 3 then
circuit_breaker.state = "open"
end
return nil, err
end
end
10. 未来发展与替代方案
10.1 Redis模块扩展
Redis可以通过模块扩展功能,如:
- RedisBloom:布隆过滤器
- RedisJSON:JSON支持
- RediSearch:全文搜索
使用示例:
lua复制-- 注册模块前缀
redis.register_module_prefix("bf") -- Bloom Filter
-- 使用Bloom Filter
local res, err = red:bf():add("user_filter", "user123")
if res == 1 then
ngx.say("新用户")
else
ngx.say("已存在用户")
end
10.2 替代方案比较
-
Redis vs Memcached:
- Redis:数据结构丰富、持久化、支持Lua脚本
- Memcached:更简单、多线程、内存利用率更高
-
Redis Cluster vs Proxy:
- Cluster:官方方案、自动分片、高可用
- Proxy(如Twemproxy):更简单、客户端无感知
-
Redis vs KeyDB:
- KeyDB:多线程、兼容Redis协议、性能更高
在实际项目中,我遇到过需要从单机Redis迁移到Redis Cluster的情况。迁移过程中最大的挑战是处理多键操作和事务,最终我们通过重构Lua脚本和使用hash tag解决了这些问题。
