香港服务器运行Windows Server时,如何通过IIS与Redis缓存结合,优化跨境电商秒杀活动的并发处理?

跨境电商平台的秒杀倒计时只剩 20 分钟,我们把最后两台 Windows Server 2019 Datacenter 的补丁打完重启,IIS 应用池升为 AlwaysRunning,Redis 哨兵状态是 ok。
微信群里,前端同学在做最后的倒计时页压测;我这边盯着 IIS 每秒请求数、Redis ops、以及 SQL Server 等待类型。这次我们决心把“削峰填谷”做彻底:资格发放在 Redis,扣减在 Lua 原子脚本,订单入队 Redis Stream 异步落库。
3 点整,秒杀开闸。第一波峰值 18 万 QPS 打过来,我深吸一口气,看着 99 线延迟从 1.3s 慢慢回落到 280ms——这次,稳了。
1. 目标与思路
目标
- 承载峰值 15–20 万 QPS 的瞬时读流量
- 订单通道可控在 5–8 千 QPS,保证库存与“每人限购 1 件”的强一致
- 把 99 线接口延迟压到 300ms 内,错误率 < 0.5%
思路
IIS 负责接入与静态加速,开启内核缓存、压缩、连接复用,扩充队列长度。
Redis 做“四板斧”:
- 限流(IP/用户双维度)
- 资格票(令牌桶/预发券)
- 原子扣减(Lua 保证库存与去重)
- 异步下单(Redis Stream 队列 + 后台消费者)
数据库只吃“平滑后的订单流”,避免被尖峰直击。
全链路观测(IIS 日志 + PerfCounter + Redis slowlog + 业务日志),及时熔断/降级。
2. 拓扑与硬件
2.1 逻辑拓扑
- 边缘/CDN(秒杀静态页、倒计时、图片):降低跨境 RTT
- L4 LVS/HAProxy(两台冗余,VIP 漂移):均衡到 IIS
- 应用层:4 台 Windows Server 2019 + IIS 10(ASP.NET Core In-Process 托管)
- 缓存层:3 节点 Redis(Ubuntu 22.04)+ Sentinel,1 主 2 从,专用内网
- DB 层:SQL Server 2019 Always On(读写分离 + 只写主库)
2.2 设备清单(实际参数示例)
| 角色 | 数量 | CPU | 内存 | 磁盘 | 网卡 | OS |
|---|---|---|---|---|---|---|
| LVS/HAProxy | 2 | Xeon Silver 4214R ×2 | 64 GB | NVMe 960GB | 10GbE | Ubuntu 22.04 |
| 应用(IIS) | 4 | Xeon Gold 6226R ×2 | 128 GB | NVMe 1.92TB | 10GbE | Windows Server 2019 Datacenter |
| Redis | 3 | Xeon Silver 4210 | 64 GB | NVMe 960GB | 10GbE | Ubuntu 22.04 |
| SQL 主 | 1 | Xeon Gold 6248R ×2 | 256 GB | SSD RAID10 7.6TB | 10GbE | Windows Server 2019 |
| SQL 从 | 1 | Xeon Gold 6248R ×2 | 256 GB | SSD RAID10 7.6TB | 10GbE | Windows Server 2019 |
网络:香港多线 BGP,核心交换 10GbE,业务网与管理网隔离;Redis、DB 与应用在同一可用区内,RTT 0.2–0.4ms。
3. Redis 层设计
3.1 部署要点(Ubuntu)
# 基础安装
apt update && apt install -y redis-server
# redis.conf 关键参数
bind 0.0.0.0
protected-mode yes
requirepass <StrongPass>
maxmemory 8gb
maxmemory-policy allkeys-lru
appendonly yes
appendfsync everysec
tcp-backlog 65535
tcp-keepalive 60
save 900 1 300 10 60 10000
哨兵(sentinel.conf)示例
port 26379
sentinel monitor ms1 10.10.10.21 6379 2
sentinel auth-pass ms1 <StrongPass>
sentinel down-after-milliseconds ms1 5000
sentinel failover-timeout ms1 60000
sentinel parallel-syncs ms1 1
经验:秒杀时 Redis 的单核性能是瓶颈关键。尽量让热点 Key 落在同一槽位,并把 Lua 脚本做成短、小、定长参数,避免大对象序列化。
3.2 Key 设计(使用哈希标签避免 CROSSSLOT)
- stock:{skuId} 库存数(String, int)
- buyers:{skuId} 已购用户集合(Set)
- orders:{skuId} 订单 Stream(XADD)
- rate:ip:{ip}:{window}、rate:u:{uid}:{window} 限流计数(String)
说明:{skuId} 用花括号包裹,确保多 Key 操作在同一哈希槽,Lua 原子脚本才不会报 CROSSSLOT。
3.3 限流(漏斗/滑动窗口)
滑动窗口(1 秒 10 次/用户)Lua:
-- KEYS[1] = rate key
-- ARGV[1] = windowSec
local cur = redis.call('INCR', KEYS[1])
if cur == 1 then
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[1]))
end
return cur
3.4 原子扣减 + 去重 + 入队(Lua)
-- KEYS[1]=buyers:{skuId} KEYS[2]=stock:{skuId} KEYS[3]=orders:{skuId}
-- ARGV[1]=uid ARGV[2]=orderId ARGV[3]=buyersTTL
if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then
return -1 -- 已购
end
local s = tonumber(redis.call('GET', KEYS[2]) or "0")
if s <= 0 then
return 0 -- 无库存
end
redis.call('DECR', KEYS[2])
redis.call('SADD', KEYS[1], ARGV[1])
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))
redis.call('XADD', KEYS[3], '*', 'uid', ARGV[1], 'oid', ARGV[2])
return 1
4. IIS 与 Windows 2019 调优
4.1 IIS 站点与应用池
应用池:
- Start Mode = AlwaysRunning
- Idle Time-out (min) = 0
- Queue Length = 50000(默认 1000,秒杀容易打满 503)
回收:固定时间错峰(比如 4:30),避免活动期回收
- 站点 Limits:
- Connection Timeout = 120
- maxConnections = 0 (Unlimited)
开启 HTTP/2(Windows 2019 支持,减少握手开销)
内核缓存与压缩
<system.webServer>
<caching enabled="true" enableKernelCache="true">
<profiles>
<add extension=".js" policy="CacheForTimePeriod" duration="01:00:00" kernelCachePolicy="CacheForTimePeriod"/>
<add extension=".css" policy="CacheForTimePeriod" duration="01:00:00" kernelCachePolicy="CacheForTimePeriod"/>
</profiles>
</caching>
<httpCompression>
<scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" />
<dynamicTypes>
<add mimeType="application/json" enabled="true" />
<add mimeType="text/*" enabled="true" />
</dynamicTypes>
</httpCompression>
</system.webServer>
4.2 TCP 参数(注册表)
目标:扩展临时端口、降低 TIME_WAIT 占用、保持长连
注册表路径 HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters:
- MaxUserPort = 65534 (DWORD)
- TcpTimedWaitDelay = 30 (DWORD)
- TcpKeepAliveInterval = 1 (DWORD)
- TcpKeepAliveTime = 600000 (DWORD, 10 分钟)
更改后重启生效。IIS 与 Redis 客户端也要开启 KeepAlive。
4.3 ASP.NET Core 托管(In-Process)
web.config 关键片段:
<configuration>
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified"/>
</handlers>
<aspNetCore processPath="dotnet" arguments="Seckill.Web.dll" stdoutLogEnabled="true" hostingModel="InProcess">
<environmentVariables>
<add name="ASPNETCORE_ENVIRONMENT" value="Production" />
<add name="DOTNET_GCServer" value="1" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</configuration>
5. 应用层实现(C# / ASP.NET Core)
5.1 依赖与连接
// Program.cs
using StackExchange.Redis;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var cfg = ConfigurationOptions.Parse("10.10.10.21:6379,10.10.10.22:6379,10.10.10.23:6379,password=StrongPass");
cfg.AbortOnConnectFail = false;
cfg.ConnectRetry = 5;
cfg.KeepAlive = 60; // 秒
cfg.AllowAdmin = false;
cfg.SyncTimeout = 5000;
cfg.ConnectTimeout = 2000;
return ConnectionMultiplexer.Connect(cfg);
});
builder.Services.AddSingleton<IDatabase>(sp => sp.GetRequiredService<IConnectionMultiplexer>().GetDatabase());
var app = builder.Build();
5.2 中间件:双维度限流(IP + 用户)
app.Use(async (ctx, next) =>
{
var db = ctx.RequestServices.GetRequiredService<IDatabase>();
var ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "0.0.0.0";
var uid = ctx.User?.Identity?.Name ?? ctx.Request.Headers["X-UID"].ToString();
var nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string ipKey = $"rate:ip:{ip}:{nowSec}";
string uKey = $"rate:u:{uid}:{nowSec}";
// 1 秒窗,IP 50 次,用户 10 次
var script = LuaScript.Prepare(@"
local cur = redis.call('INCR', KEYS[1])
if cur == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end
return cur
");
var ipCnt = (long)db.ScriptEvaluate(script, new RedisKey[] { ipKey }, new RedisValue[] { 1 });
var uCnt = (long)db.ScriptEvaluate(script, new RedisKey[] { uKey }, new RedisValue[] { 1 });
if (ipCnt > 50 || uCnt > 10)
{
ctx.Response.StatusCode = StatusCodes.Status429TooManyRequests;
ctx.Response.Headers["Retry-After"] = "1";
await ctx.Response.WriteAsync("Too Many Requests");
return;
}
await next();
});
5.3 原子扣减 + 入队下单
app.MapPost("/api/seckill/{skuId}", async (HttpContext ctx, string skuId, IDatabase db) =>
{
var uid = ctx.User?.Identity?.Name ?? ctx.Request.Headers["X-UID"].ToString();
if (string.IsNullOrEmpty(uid)) return Results.Unauthorized();
var buyers = (RedisKey)$"buyers:{{{skuId}}}";
var stock = (RedisKey)$"stock:{{{skuId}}}";
var stream = (RedisKey)$"orders:{{{skuId}}}";
var orderId = Guid.NewGuid().ToString("N");
var script = LuaScript.Prepare(@"
if redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then return -1 end
local s = tonumber(redis.call('GET', KEYS[2]) or '0')
if s <= 0 then return 0 end
redis.call('DECR', KEYS[2])
redis.call('SADD', KEYS[1], ARGV[1])
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))
redis.call('XADD', KEYS[3], '*', 'uid', ARGV[1], 'oid', ARGV[2])
return 1
");
var ret = (long)db.ScriptEvaluate(script,
new RedisKey[] { buyers, stock, stream },
new RedisValue[] { uid, orderId, 86400 });
return ret switch
{
-1 => Results.Conflict("Duplicate purchase"),
0 => Results.Gone("Sold out"),
1 => Results.Accepted($"/api/order/{orderId}", new { orderId }),
_ => Results.StatusCode(500)
};
});
5.4 后台消费者(订单落库)
// Worker: 预先创建消费组 XGROUP CREATE orders:{sku} g1 $ MKSTREAM
while (true)
{
var res = await db.StreamReadGroupAsync($"orders:{{{skuId}}}", "g1", "c1",
new RedisValue(">").ToString(), count: 200, autoAcknowledge: false);
foreach (var msg in res)
{
var uid = msg.Values.First(v => v.Name == "uid").Value.ToString();
var oid = msg.Values.First(v => v.Name == "oid").Value.ToString();
// 幂等落库(以订单号为幂等键)
await SaveOrderIfNotExistsAsync(oid, uid, skuId);
await db.StreamAcknowledgeAsync($"orders:{{{skuId}}}", "g1", msg.Id);
}
}
6. 压测与效果
6.1 测试场景
- 读流量:倒计时页 + 商品详情 + 库存查询,峰值 180k QPS
- 下单接口:峰值 8k QPS(被资格与队列限流后)
- 网络:中国内地多省移动/联通/电信,香港回源 RTT 30–80ms
6.2 前后对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 峰值下单 99 线延迟 | 1.8 s | 280 ms |
| 错误率(5xx/4xx) | 3.2% | 0.37% |
| IIS 队列溢出(503) | 频繁 | 无(QueueLength 50k) |
| Redis CPU 峰值 | 95%(单核打满) | 62%(脚本瘦身 + 同槽) |
| SQL 写入峰值 | 11k QPS(抖动) | 6.5k QPS(平滑) |
7. 现场遇到的坑与解法
IIS 503 突发
现象:活动开始第 3 分钟出现少量 503。
原因:应用池队列满(默认 1000)。
处理:Queue Length = 50000,同时前置限流,使应用层保持可用。
Redis -CROSSSLOT 报错
现象:早期用普通 key,Lua 多 Key 操作报错。
解法:统一使用 buyers:{skuId}、stock:{skuId}、orders:{skuId},哈希标签强制同槽。
Redis CPU 飙升
现象:XADD 字段过多 + Lua 拼接大 JSON。
解法:Stream 只写必要字段(uid, oid),大对象放 DB;脚本出参改短整型。
TIME_WAIT 堆积
现象:应用服务器临时端口耗尽导致连接失败。
解法:注册表扩端口 + 降低 TcpTimedWaitDelay,并强制启用 KeepAlive。
应用池回收卡点
现象:IIS 默认回收导致瞬时 502。
解法:把回收时间错峰到活动结束后;开启 Overlapped Recycling 但避免在高峰触发。
DB 死锁与热点
现象:订单插入时对库存表竞争严重。
解法:库存以 Redis 为主,DB 仅做最终一致;订单表分区 + 索引覆盖 + 幂等写。
8. 运营/风控与降级策略
- 多级限流:CDN 边缘(WAF/IP 黑白)、LVS(连接)、IIS(队列)、应用(用户/接口)
- 灰度/熔断:下单通道降至 3k QPS、只读详情页继续服务
- 幂等:订单以 orderId 或 (uid, skuId) 做幂等键
- 降级页:预置“排队中”页面 + Retry-After 指引
观测:
- IIS:Requests/Sec、Current Connections、Queue Length
- Redis:instantaneous_ops_per_sec、used_cpu_sys、rejected_connections、slowlog
- 业务:Lua 返回码分布、Stream 堵塞长度、消费者滞后
9. 标准化上线清单(可直接对表执行)
Redis
- 主从 + Sentinel 正常切换演练
- maxmemory、appendonly、appendfsync everysec
- 热点 Key 使用 {} 哈希标签
- slowlog 开启并阈值 ≤ 5ms
IIS / Windows
- 应用池 AlwaysRunning、QueueLength >= 50000、回收错峰
- HTTP/2、内核缓存、Gzip 动态压缩
- 注册表 MaxUserPort、TcpTimedWaitDelay、KeepAlive
- 文件句柄/事件日志预留磁盘空间(压测中很容易爆)
应用
- 双维度限流中间件
- Lua 原子脚本 + Stream 入队
- 消费者幂等、重试、死信监控
- 降级开关(配置中心/环境变量)
压测
- 读压峰值 ≥ 预期 1.2 倍
- 下单峰值以“资格”控制在可处理区间
- 99 线延迟、错误率、GC 次数达标
10. FAQ 小抄
为什么不用数据库扣减?
秒杀是“尖峰 + 冲突”典型场景,DB 原子扣减写放大、锁竞争严重;把强一致前置到 Redis(原子 + 单线程)是更稳的思路,DB 只负责最终一致与审计。
Redis Stream vs List?
Stream 有消费组、位点管理、可观测性更好;List 简单但容错差。我们线上用 Stream。
StackExchange.Redis 连接池需要吗?
官方 Multiplexer 已复用连接,不要每次请求创建连接。统一 Singleton。
结尾|清晨 4:15 的出风口
风口的冷气有点呛,我把最后一条告警静音,Redis 的 ops 从 20 万/秒慢慢降下来,IIS 的队列也见底了。群里开始刷用户晒单与客服感谢。
这夜我们没有奇技淫巧,只是在Windows 2019 + IIS这条老牌栈上,把“限流—资格—原子—异步”的路径走到了极致。
隔着机房玻璃,我看见港铁的第一班车穿过黑夜。那一刻我知道,下次秒杀来时,这套系统仍然能稳住——因为它是用一晚上、一行行日志、一遍遍压测换来的。
附:可复用配置与代码清单(便于拷贝)
1) redis.conf 关键项
bind 0.0.0.0
requirepass <StrongPass>
maxmemory 8gb
maxmemory-policy allkeys-lru
appendonly yes
appendfsync everysec
tcp-backlog 65535
tcp-keepalive 60
2) 哨兵 sentinel.conf
sentinel monitor ms1 10.10.10.21 6379 2
sentinel auth-pass ms1 <StrongPass>
sentinel down-after-milliseconds ms1 5000
sentinel failover-timeout ms1 60000
3) IIS web.config 片段(In-Process)
<aspNetCore processPath="dotnet" arguments="Seckill.Web.dll" stdoutLogEnabled="true" hostingModel="InProcess"/>
4) 限流中间件(C#) — 见上文 5.2
5) 原子扣减 Lua — 见上文 3.4
6) 订单消费者骨架 — 见上文 5.4
如果你也要在香港服务器 + Windows Server 2019 + IIS + Redis上扛一场跨境秒杀,照着本文把拓扑—参数—代码—压测四件事走通,基本就能把系统从“能跑”推到“能扛”。