上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-15 09:09 阅读量:639


我们有一个客户在美国西海岸做跨境电商,一直以来,投诉点只有一个:购物车加载慢。他们的应用在香港 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 深化。等到电梯门合上的那一刻,希望你和我一样——疲惫,但很踏实。

目录结构
全文