如何在香港服务器的Ubuntu系统中结合Redis缓存与Nginx,提升电商网站海外用户的购物车加载速度?

我们有一个客户在美国西海岸做跨境电商,一直以来,投诉点只有一个:购物车加载慢。他们的应用在香港 Ubuntu 服务器上,跨太平洋 RTT 在 120–160ms 徘徊。前端每次点开购物车就要拉总价、库存、优惠、运费估算,十几个 API 连环调用,后端是 PHP(Laravel)+ MySQL,没做任何实质缓存。午夜 1:40,我开着远程 KVM,看着 Nginx access log 里一条条 /api/cart/summary 的 500ms+,心里很清楚:这活儿必须用 Redis + Nginx 组合拳。
下面是我当晚到次日清晨的完整落地过程,从硬件、系统、Redis 设计、Nginx 配置、到压测和坑位复盘。我尽量把步骤写细,无论新手老手,都能照着做。
1. 现场条件与目标
1.1 机型与网络(实参)
| 组件 | 参数 |
|---|---|
| 机房 | 香港(CN2/GIA 线路,国际出口 1 Gbps 共享) |
| CPU | AMD EPYC 7702 8 vCPU(虚拟化) |
| 内存 | 32 GB |
| 存储 | 2 × 1TB NVMe(RAID1,EXT4) |
| 系统 | Ubuntu Server 22.04 LTS,内核 5.15 |
| 端口 | 1 Gbps,保证带宽 200 Mbps |
| 典型 RTT | SG: 35ms、Tokyo: 55ms、LA: 140ms、London: 190ms |
1.2 基线指标(接手时)
| 指标 | 值 | 说明 |
|---|---|---|
| /api/cart/summary P95 | 620 ms | 无缓存,后端多次读库、计算促销 |
| /api/cart/items P95 | 480 ms | 同步查存货,产品信息重复读取 |
| 峰值 QPS | 420 | 大促期间抓不住 |
| 购物车页可视加载(FCP→可用) | 2.6 s → 5.3 s | 海外用户(US West) |
1.3 优化目标(上线当晚)
- /api/cart/summary P95 ≤ 180ms(US West)
- 峰值 QPS 提升 ≥ 2 倍 不扩容
- 购物车页可视加载缩短 ≥ 40%
2. 架构与思路
要点:把“常变中有不变”的部分尽量缓存到 Redis,把“高频瞬时动态”的接口交给 Nginx 做微缓存或直连 Redis 返回。
应用侧 Redis:
- 热点产品快照(价格、标题、缩略图、类目)product:snap:{sku},TTL 5–15 分钟
- 库存读缓冲 stock:buf:{sku},TTL 10–30 秒(最终一致)
- 用户购物车原始结构 cart:{session}(写入/合并逻辑仍在应用)
Nginx 层:两条路
- 纯 Nginx 微缓存(proxy_cache):对 只读 GET 的“汇总与展示”接口(如 /api/cart/summary)做“按用户键”微缓存 1–3 秒,吸收前端轮询与重复点击。
- OpenResty(Lua) 直连 Redis:对于能在边缘层直接拼装/返回的轻量 JSON(如购物车计数、金额快照),绕开应用,降低后端压力与跨语言开销。
核心风险是按用户缓存键设计错误会导致数据泄露。下面的配置里会把 Session Cookie 严格打入 cache key。
3. 环境准备
3.1 基础依赖
# 系统更新
sudo apt update && sudo apt -y upgrade
# 安装 Nginx 与 Redis
sudo apt -y install nginx redis-server
# 可选:安装 OpenResty(用于 Lua)
# 官方源:https://openresty.org/en/linux-packages.html
sudo apt -y install gnupg ca-certificates
wget -qO - https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/openresty.gpg
echo "deb [signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu jammy main" \
| sudo tee /etc/apt/sources.list.d/openresty.list
sudo apt update && sudo apt -y install openresty
# 压测工具(任选)
sudo apt -y install wrk jq
如果你暂不使用 OpenResty,也能先用“纯 Nginx 微缓存方案”。
3.2 系统内核与网络优化(Ubuntu)
# /etc/sysctl.d/99-tuning.conf
sudo tee /etc/sysctl.d/99-tuning.conf >/dev/null <<'EOF'
# BBR 拥塞控制
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# 连接与队列
net.core.somaxconn = 10240
net.core.netdev_max_backlog = 16384
net.ipv4.tcp_max_syn_backlog = 8192
# TIME-WAIT 优化
net.ipv4.tcp_tw_reuse = 1
# 端口范围
net.ipv4.ip_local_port_range = 10000 65000
# Keepalive
net.ipv4.tcp_keepalive_time = 120
net.ipv4.tcp_fin_timeout = 15
EOF
sudo sysctl --system
4. Redis:键设计、配置与安全
4.1 键与 TTL 设计(示例)
键 含义 TTL 备注
product:snap:{sku} 商品快照(价/图/名/类目) 600s 后台改价时用消息或接口清理
stock:buf:{sku} 库存读缓冲 20s 库存最终一致,短期缓冲
cart:{session} 用户购物车原始数据 7d 登录/访客均以 session 维度
cart:sum:{session} 购物车汇总(金额/件数) 2–5s Nginx 可直接读
promo:rule:v{n} 促销规则版本化 900s 有版本号即可热更新
4.2 redis.conf 关键配置
# /etc/redis/redis.conf(关键片段)
bind 127.0.0.1
protected-mode yes
port 6379
# 如需更安全可用 unixsocket + 权限控制:
# unixsocket /var/run/redis/redis.sock
# unixsocketperm 770
# 内存与淘汰策略(示例 8GB)
maxmemory 8gb
maxmemory-policy allkeys-lru
# 持久化:缓存为主建议禁用 AOF,保留 RDB 低频快照以便冷启动
save 900 1
save 300 100
save 60 10000
appendonly no
# 连接队列
tcp-backlog 8192
timeout 0
tcp-keepalive 300
# 日志
logfile /var/log/redis/redis-server.log
安全要点:Redis 只监听本机,不要对外网开放;若必须跨主机,务必加 stunnel/内网专线或使用 requirepass 与防火墙。
重启生效:
sudo systemctl restart redis-server
sudo systemctl enable redis-server
5. 方案 A:纯 Nginx 微缓存(proxy_cache,按用户缓存键)
这个方案部署最快,对现有应用零侵入。思路是:针对 GET /api/cart/summary 等接口,Nginx 几秒级微缓存(带 Session 维度)。前端在同一可视阶段会多次请求(比如倒计时轮询、重复打开购物车抽屉),我们用 1–3 秒吸收这些浪涌。
5.1 缓存目录与全局设置
# /etc/nginx/nginx.conf(关键片段)
http {
# 基础优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# 压缩
gzip on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types application/json application/javascript text/css text/plain;
# HTTP/3 可选(openresty 或 nginx-quic)
# quic_retry on;
# 代理缓存路径
proxy_cache_path /var/cache/nginx/cart levels=1:2 keys_zone=cart_cache:256m
inactive=10s max_size=10g use_temp_path=off;
# 自定义日志(含上游耗时)
log_format upstream_timing '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent "$http_referer" '
'"$http_user_agent" upstream:$upstream_addr '
'rt:$request_time urt:$upstream_response_time cache:$upstream_cache_status';
access_log /var/log/nginx/access.log upstream_timing;
}
5.2 站点配置(按 cookie 维度的 cache key)
# /etc/nginx/sites-available/shop.conf
server {
listen 80;
server_name shop.example.com;
# HTTPS 建议配合 443 + HTTP/2/3,此处略
set $session_cookie $cookie_sessionid; # 你的 Session Cookie 名
if ($session_cookie = "") { set $session_cookie $cookie_PHPSESSID; }
if ($session_cookie = "") { set $session_cookie $remote_addr; } # 兜底
# 上游
upstream app_backend {
server 127.0.0.1:9000; # PHP-FPM 或 Node 端口
keepalive 64;
}
# 只对 GET 的汇总类接口微缓存(1-3s)
location = /api/cart/summary {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 缓存键:协议+主机+URI+查询串+Session
proxy_cache cart_cache;
proxy_cache_key "$scheme://$host$request_uri|$session_cookie";
proxy_cache_valid 200 3s; # 成功缓存 3s
proxy_cache_valid 301 302 1m;
proxy_cache_valid any 0s; # 其他不缓存
# 避免缓存头像等私密(这里仅对该接口)
proxy_ignore_headers Set-Cookie;
add_header X-Cache-Status $upstream_cache_status;
# 禁止对非常见状态码缓存
proxy_cache_lock on; # 防止惊群
proxy_cache_lock_timeout 2s;
# POST/带鉴权头的请求不缓存(如带 Authorization)
if ($request_method != GET) { set $skip_cache 1; }
if ($http_authorization != "") { set $skip_cache 1; }
proxy_no_cache $skip_cache;
proxy_cache_bypass $skip_cache;
}
# 其他接口直透
location / {
proxy_pass http://app_backend;
}
}
关键点:
- proxy_cache_key 把 $session_cookie 打进来,避免用户间串缓存。
- proxy_cache_lock on 可防缓存击穿。
- 只对 GET 汇总 做微缓存,POST/PUT/PATCH/DELETE 一律直透。
- 后端响应可配 Cache-Control: private, max-age=3,但这里以反向代理规则为准。
启用站点:
sudo ln -s /etc/nginx/sites-available/shop.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
6. 方案 B:OpenResty(Nginx+Lua)直连 Redis,边缘拼装返回
当 /api/cart/summary 的结构足够简单(例如:金额、件数、优惠名),我们可以在 Nginx 层直接从 Redis 取 cart:sum:{session},命中则直接返回 JSON;未命中再回源,回写 Redis,TTL 2–5 秒,减少后端压力。
6.1 Lua 代码(直连 Redis)
-- /etc/nginx/lua/cart_cache.lua
local cjson = require "cjson.safe"
local redis = require "resty.redis"
local function get_session()
local ck = ngx.var.cookie_sessionid or ngx.var.cookie_PHPSESSID or ngx.var.remote_addr
return ck
end
local function redis_conn()
local red = redis:new()
red:set_timeout(50) -- 50ms 连接/命令超时
assert(red:connect("127.0.0.1", 6379))
-- 如有密码: red:auth("yourpass")
return red
end
-- 读取 Redis
local session = get_session()
local key = "cart:sum:" .. session
local red = redis_conn()
local val, err = red:get(key)
if val and val ~= ngx.null then
ngx.header.content_type = "application/json; charset=utf-8"
ngx.print(val)
return ngx.exit(200)
end
-- 未命中 -> 回源
local res = ngx.location.capture("/backend/cart/summary_internal")
if res.status == 200 then
-- 回写 Redis,短 TTL,防抖
red:setex(key, 3, res.body)
end
ngx.status = res.status
for k, v in pairs(res.header) do
ngx.header[k] = v
end
ngx.print(res.body)
6.2 Nginx(OpenResty)位置块
# /usr/local/openresty/nginx/conf/nginx.conf(片段)
http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# ...其余同前
server {
listen 80;
server_name shop.example.com;
# 对外接口:优先读 Redis
location = /api/cart/summary {
content_by_lua_file /etc/nginx/lua/cart_cache.lua;
}
# 内部回源接口(避免递归)
location = /backend/cart/summary_internal {
internal;
proxy_pass http://app_backend/api/cart/summary;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# 其他接口
location / {
proxy_pass http://app_backend;
}
}
}
这种方式比微缓存更“可控”,可以把 Redis 命中率打得更高,也便于在 Lua 里做细粒度降级、AB 标或者熔断。
7. 应用侧(以 PHP/Laravel 为例)的 Redis 接入要点
目标:把“重计算”的东西(促销规则、商品快照)放入 Redis,购物车写仍走数据库/业务逻辑,购物车读汇总尽量从 Redis 吃。
// 伪代码示意
function getCartSummary($session) {
$redisKey = "cart:sum:$session";
$cached = redis()->get($redisKey);
if ($cached) return json_decode($cached, true);
$cart = loadCartFromDB($session); // 读取用户购物车原始结构
$products = preloadProductsFromRedis(array_keys($cart->items)); // 批量从 product:snap:{sku} 读
$stock = preloadStockBuf(array_keys($cart->items)); // stock:buf:{sku}
$summary = compute_summary($cart, $products, $stock, current_promo_version());
// 短 TTL 回写,避免击穿
redis()->setEx($redisKey, 3, json_encode($summary));
return $summary;
}
避免缓存雪崩/击穿:
- 使用 短 TTL + 随机抖动(例如 3–5 秒随机)
- OpenResty 侧 proxy_cache_lock 或 Lua 的 分布式锁(lua-resty-lock)
- 促销规则用 promo:rule:v{n} 版本化,更新时只需 bump 版本并清理相关键。
8. 缓存失效策略(清理命中)
- 商品变价:后台改价后,异步任务清理 product:snap:{sku},并广播消息(如 Redis Pub/Sub 或消息队列)供边缘层清理 cart:sum:* 相关键(按 session 清理较重,可接受 2–5 秒过渡)。
- 库存瞬时:坚持用 stock:buf:{sku} 的短 TTL(10–30 秒),下单会再进行最终库存扣减校验。
- 促销规则:版本号递增,计算汇总时带上 vN,只要 bump 版本,旧汇总几秒内自然过期。
9. 监控与压测
9.1 Nginx 日志采样
log_format 已加入 upstream_response_time 与 upstream_cache_status,可快速看命中率:
# 查看 5 分钟内 /api/cart/summary 命中率
sudo awk '/\/api\/cart\/summary/ {print $0}' /var/log/nginx/access.log \
| awk '{for(i=1;i<=NF;i++) if ($i ~ /^cache:/) print $i}' \
| cut -d: -f2 | sort | uniq -c
9.2 压测(US West 模拟,微缓存受益明显)
# 以 140ms RTT 模拟(本地 wrk -> tc netem 或海外机器执行)
wrk -t8 -c400 -d2m --latency http://shop.example.com/api/cart/summary
上线前后对比(真实一次活动前夜):
| 指标 | 优化前 | 方案 A(微缓存) | 方案 B(OpenResty+Redis) |
|---|---|---|---|
| /api/cart/summary P95 | 620 ms | 190 ms | 150 ms |
| /api/cart/summary P99 | 900 ms | 320 ms | 260 ms |
| 峰值 QPS | 420 | 980 | 1,300 |
| 购物车页可视加载 | 2.6→5.3 s | 1.6→3.0 s | 1.3→2.4 s |
备注:方案 B 的优势在高并发与后端计算重时更明显,但实施复杂度更高。
10. 性能优化细项清单(容易忽略但很有效)
Upstream keepalive:
upstream app_backend { keepalive 64; } + proxy_http_version 1.1; proxy_set_header Connection "";
避免 Nginx→后端反复握手。
压缩:JSON 响应开启 gzip(或 brotli),跨境带宽成本高时收益大。
HTTP/2/3:海外长距离链路多路复用收益明显(证书与 ALPN 配合)。
文件句柄与 worker:
# /etc/security/limits.d/nginx.conf
nginx soft nofile 200000
nginx hard nofile 200000
Nginx worker_processes auto; worker_connections 8192;。
Redis 本地连接:优先 127.0.0.1 或 unix socket,减少网络开销。
TTL 抖动:ttl = base + random(0..base/2),降低同时过期的雪崩。
11. 常见坑与我的“现场修复”
| 坑位 | 现象 | 根因 | 处理 |
|---|---|---|---|
| 缓存串号 | A 用户看到 B 的购物车 | 缓存键未包含 session/cookie | proxy_cache_key 加入 $cookie_sessionid,Lua 侧按 session key |
| 命中率不稳 | 缓存命中忽高忽低 | 前端携带随机查询串/时间戳 | 统一前端请求,剔除无意义 query 参数(或在 Nginx 端 map 过滤) |
| 规则更新滞后 | 改价不生效 | 无清理或 TTL 太长 | 商品快照 TTL 10 分钟+显式失效;汇总 TTL 3 秒 |
| 库存不准 | 大促时少量超卖 | 读缓冲存在延迟 | 下单前强校验库存并原子扣减;读缓冲仅做体验优化 |
| CPU 飙高 | JSON 组装压力大 | 后端重复计算 | “汇总缓存”前移到边缘层;产品快照批量预取 |
| AOF 卡顿 | Redis 写抖动 | AOF fsync 压力 | 以缓存为主时禁用 AOF,仅保留 RDB 低频快照 |
12. 一键化部署脚本(最小可用)
用于在已安装 Nginx/Redis 的前提下,下发微缓存配置。请按需修改 Cookie 名与后端端口。
#!/usr/bin/env bash
set -e
SESSION_COOKIE=${SESSION_COOKIE:-sessionid}
APP_UPSTREAM=${APP_UPSTREAM:-127.0.0.1:9000}
sudo mkdir -p /var/cache/nginx/cart
sudo chown -R www-data:www-data /var/cache/nginx
sudo tee /etc/nginx/conf.d/cart_cache.conf >/dev/null <<EOF
proxy_cache_path /var/cache/nginx/cart levels=1:2 keys_zone=cart_cache:256m inactive=10s max_size=10g use_temp_path=off;
EOF
sudo tee /etc/nginx/sites-available/shop.conf >/dev/null <<EOF
upstream app_backend { server ${APP_UPSTREAM}; keepalive 64; }
server {
listen 80;
server_name _;
set \$session_cookie \$cookie_${SESSION_COOKIE};
if (\$session_cookie = "") { set \$session_cookie \$cookie_PHPSESSID; }
if (\$session_cookie = "") { set \$session_cookie \$remote_addr; }
location = /api/cart/summary {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_cache cart_cache;
proxy_cache_key "\$scheme://\$host\$request_uri|\$session_cookie";
proxy_cache_valid 200 3s;
proxy_cache_valid any 0s;
proxy_ignore_headers Set-Cookie;
proxy_cache_lock on;
proxy_cache_lock_timeout 2s;
add_header X-Cache-Status \$upstream_cache_status;
}
location / { proxy_pass http://app_backend; }
}
EOF
sudo ln -snf /etc/nginx/sites-available/shop.conf /etc/nginx/sites-enabled/shop.conf
sudo nginx -t && sudo systemctl reload nginx
echo "Done. Micro-cache for /api/cart/summary enabled."
13. 成果复盘:数据说话
上线后 48 小时,我把日志丢进分析脚本里,得到如下结果(US West 样本):
| 指标 | 上线前 | 上线后(A/B 50%) |
|---|---|---|
| /api/cart/summary 命中率 | 0% | 68%(方案 A) / 82%(方案 B) |
| P95 响应 | 620 ms | 190 ms / 150 ms |
| 峰值 QPS | 420 | > 1000 |
| 购物车页可视加载 | 2.6→5.3 s | 1.4→2.7 s |
业务侧反馈:转化率在大促期间有可感知提升,客服对“购物车转圈”的抱怨显著减少。
14. 清晨 6:10 的电梯
清晨 6 点,机房的灯还刺眼。我合上笔记本,电梯镜子里的人眼睛通红,但心里是轻的。那一夜,我们没有靠“神奇中间件”或“多加几台机器”糊弄过去,而是把“该缓存的缓存掉、该微缓存的微缓存掉”。
在跨境链路的物理延迟面前,Redis + Nginx 像两块咬合严密的齿轮:一块负责记忆,一块负责提速。当你把键设计对了、把 TTL 拿捏好、把边缘逻辑下沉到位,购物车就不再是用户的等待室,而是直接通向“结账”的高速道。
附:检查清单(上线前 5 分钟快速 Review)
- proxy_cache_key 已含 Session/Cookie,杜绝串号
- 仅缓存 GET 汇总,写操作全部直透
- TTL 短 + 抖动,已开启 proxy_cache_lock / Lua 锁
- 商品/促销/库存各自 TTL 与失效策略清晰
- Nginx→Upstream Keepalive 正常
- Redis 仅本地监听 + 防火墙 +(可选)密码
- access.log 已含 $upstream_cache_status,能观测命中率
- 压测覆盖跨境 RTT(真实或模拟)
如果你也在香港的机房里,为海外用户的“购物车”发愁,不妨照着上面的步骤跑一遍。先用方案 A 见效,再考虑方案 B 深化。等到电梯门合上的那一刻,希望你和我一样——疲惫,但很踏实。