商品预约:用户进入商品详情页面,获取购买资格,并等待商品抢购倒计时。
等待抢购:等待商品抢购倒计时,直到商品开放抢购。
商品抢购:商品抢购倒计时结束,用户提交抢购订单,排队等待抢购结果,抢购成功后,扣减系统库存,生成抢购订单。
订单支付:等待用户支付成功后,系统更新订单状态,通知用户购买成功。
根据不同的业务流程阶段,逐一分析一下每个环节可能存在的技术挑战
在高并发量的情况下,让每个用户都能得到抢购资格 ?
用户预约成功之后,在商品详情页面中,会存在一个抢购倒计时,这个倒计时的初始时间是从服务端获取的,用户点击购买按钮时,系统还会去服务端验证是否已经到了抢购时间。
在等待抢购阶段,流量突增,因为在抢购商品之前(尤其是临近开始抢购之前的一分钟内),大部分用户会频繁刷新商品详情页,商品详情页面的读请求量剧增, 如果商品详情页面没有做好流量控制,就容易成为整个预约抢购系统中的性能瓶颈点
在商品抢购阶段,用户会点击提交订单,这时,抢购系统会先校验库存,当库存足够时,系统会先扣减库存,然后再生成订单。在这个过程中,短时间之内提交订单的写流量非常高
在用户支付订单完成之后,一般会由支付平台回调系统接口,更新订单状态。在支付回调成功之后,抢购系统还会通过异步通知的方式,实现订单更新之外的非核心业务处理,比如积分累计、短信通知等
在商品预约阶段中,高并发场景下需要保证用户预约资格的公平性和可靠性,同时允许预约量超过实际库存。
在商品预约阶段中,高并发场景下需要保证用户预约资格的公平性和可靠性,同时允许预约量超过实际库存。以下是基于分布式锁技术(参考第06讲内容)的完整技术分析,以及针对 Redis 单点故障问题的补充解决方案:
// 伪代码示例:用户预约资格发放
public boolean reserveCommodity(String userId, String itemId) {
// 生成唯一锁标识
String lockKey = "reserve_lock:" + itemId;
String clientId = UUID.randomUUID().toString();
try {
// 尝试获取分布式锁(设置超时防止死锁)
boolean locked = redis.set(lockKey, clientId, "NX", "PX", 10000);
if (!locked) return false;
// ------ 临界区操作(原子性保障) ------
// 1. 检查是否已预约(防重复)
if (redis.sismember("reserved_users:" + itemId, userId)) {
return false;
}
// 2. 发放预约资格(允许超库存预约)
redis.sadd("reserved_users:" + itemId, userId); // 记录预约用户
redis.incr("reserve_count:" + itemId); // 统计预约总数
return true;
// -----------------------------------
} finally {
// 释放锁(Lua脚本保证原子性)
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(clientId));
}
}
reserve_lock:itemId
),避免全局锁竞争。sadd/sismember
)记录已预约用户。incr
统计预约量,不依赖实际库存限制。PX 10000
),避免客户端异常导致死锁。针对单点故障问题,需引入 RedLock 算法(参考第06讲),基于多个独立 Redis 节点实现分布式锁。
// 伪代码:RedLock 实现
public boolean tryRedLock(String itemId, String clientId, int ttlMs) {
List<RedisNode> redisNodes = getRedisClusterNodes(); // 获取所有Redis节点
long startTime = System.currentTimeMillis();
int successCount = 0;
// 向所有节点发起加锁请求
for (RedisNode node : redisNodes) {
if (node.setLock(lockKey, clientId, ttlMs)) {
successCount++;
}
}
// 计算加锁耗时
long elapsedTime = System.currentTimeMillis() - startTime;
// 判定条件:多数节点加锁成功,且耗时小于锁有效期
boolean locked = (successCount >= redisNodes.size() / 2 + 1)
&& (elapsedTime < ttlMs);
if (locked) {
// 锁有效期需补偿网络耗时:ttlMs - elapsedTime
scheduleLockExpiration(clientId, ttlMs - elapsedTime);
return true;
} else {
// 加锁失败,释放已获得的锁
releasePartialLocks(redisNodes, lockKey, clientId);
return false;
}
}
设计点 | 说明 |
---|---|
独立节点部署 | 使用至少5个独立的 Redis 主节点(非集群模式),降低同时故障概率 |
时钟同步要求 | 各节点需使用 NTP 服务保证时钟同步,避免锁过期时间计算偏差 |
锁有效期补偿 | 锁实际有效时间 = 初始 TTL - 加锁耗时,防止因网络延迟导致锁提前失效 |
失败回滚机制 | 加锁失败时需异步释放已获得的锁,避免残留锁数据 |
场景 | 技术方案 | 优缺点 |
---|---|---|
预约量较小(QPS < 1万) | 单 Redis 节点 + 哨兵模式 | 实现简单,但主从切换时存在短暂不可用 |
高可靠性要求(金融场景) | RedLock + 5个独立 Redis 节点 | 高可用,但实现复杂、性能较低(需多节点交互) |
超高性能要求(QPS > 10万) | Redis 集群 + 细粒度锁(按用户ID分片) | 通过分片提升并发能力,但需解决数据倾斜问题 |
无锁化设计尝试:
INCR
原子操作统计预约总数,替代分布式锁。SETNX user_reserved:userId:itemId 1
实现用户级防重复预约。熔断降级策略:
监控与告警:
在商品预约阶段,通过分布式锁控制资格发放时需综合考虑:
实际工程中,若允许极低概率的重复预约,可优先使用单 Redis 节点 + 哨兵模式;若需强一致,则选择 RedLock + fencing token 组合方案。
在等待抢购阶段,应对流量突增问题需综合运用页面静态化与服务端限流策略,并结合动态内容优化与用户体验设计。
<html>
<body>
<div id="product-image">div>
<div id="countdown-timer">div>
<script>
// 异步获取倒计时
fetch('/api/countdown?itemId=1001')
.then(response => response.json())
.then(data => updateCountdown(data));
script>
body>
html>
main.[hash].css
)实现版本更新。/api/countdown
),设置短时间缓存(如1秒),确保用户看到准实时数据。let serverTime = 1664000000; // 服务端返回的抢购开始时间戳
let localOffset = Date.now() / 1000 - serverTime;
function updateCountdown() {
let remaining = serverTime - Math.floor(Date.now() / 1000 - localOffset);
if (remaining <= 0) {
showBuyButton(); // 显示购买按钮
} else {
displayTimer(remaining); // 显示倒计时
setTimeout(updateCountdown, 1000);
}
}
层级 | 限流策略 | 工具/实现 |
---|---|---|
CDN/边缘节点 | 限制同一IP的请求频率(如10次/秒) | 阿里云CDN频率控制、Cloudflare Rate Limiting |
网关层 | 按API维度限制QPS(如商品详情页动态接口10万QPS) | Nginx limit_req (漏桶算法)、Spring Cloud Gateway RequestRateLimiter |
应用层 | 基于用户ID或设备指纹的细粒度限流(如单个用户每秒最多5次请求) | Redis + Lua脚本(计数器算法) |
服务层 | 熔断非核心服务(如推荐系统、用户画像),保障抢购链路资源 | Hystrix、Sentinel |
http {
limit_req_zone $binary_remote_addr zone=product_detail:10m rate=100r/s;
server {
location /api/countdown {
limit_req zone=product_detail burst=20 nodelay;
proxy_pass http://backend_servers;
}
}
}
limit_req_zone
:定义限流区域(按IP),10MB内存存储状态,允许100请求/秒。burst=20
:允许突发20个请求进入队列。nodelay
:突发请求不延迟处理,直接拒绝超限请求。// 前端处理限流响应
fetch('/api/countdown')
.catch(error => {
if (error.status === 429) {
showQueuePage(); // 显示排队页,倒计时后重试
}
});
let retries = 0;
function fetchWithRetry() {
fetch('/api/countdown')
.catch(() => {
setTimeout(() => {
retries++;
fetchWithRetry();
}, Math.min(1000 * 2 ** retries, 30000));
});
}
// 长轮询示例
function longPollCountdown() {
fetch('/api/countdown?longPoll=true')
.then(response => {
updateCountdown(response.data);
longPollCountdown(); // 递归调用
})
.catch(() => setTimeout(longPollCountdown, 5000));
}
// Cloudflare Worker脚本
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const cache = caches.default;
let response = await cache.match(request);
if (!response) {
response = await fetch(request);
response = new Response(response.body, response);
// 缓存1秒,确保各节点数据准实时
response.headers.append('Cache-Control', 'max-age=1');
event.waitUntil(cache.put(request, response.clone()));
}
return response;
}
指标 | 静态化+限流方案 | 传统动态渲染方案 |
---|---|---|
服务端负载 | 降低80%以上动态请求(仅处理倒计时API) | 所有请求需动态生成页面,压力大 |
用户体验 | 页面加载快,但倒计时依赖API(需处理失败场景) | 页面加载慢,但数据实时性高 |
开发复杂度 | 需维护静态生成工具与动态API协同 | 开发简单,直接渲染动态页面 |
成本 | CDN费用增加,服务器成本降低 | 服务器成本高,CDN费用低 |
等待抢购阶段的流量突增问题需通过动静分离与分层限流综合解决:
在商品抢购阶段,面对瞬时高并发流量,需通过多层次架构设计保障系统的高可用性与数据一致性。主要依赖流量削峰、扣减库存、分库分表三大核心方案。
防消息丢失:
publisher confirms
或Kafka的acks=all
。消息积压处理:
max.poll.records=500
)。消息去重:
String orderId = "ORDER_" + snowflake.nextId();
if (redis.setnx("order:dedup:" + orderId, "1") == 1) {
processOrder(orderId);
}
架构选择:
库存扣减Lua脚本:
-- KEYS[1]: 库存Key(stock:item_1001)
-- ARGV[1]: 扣减数量(通常为1)
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
多级缓存兜底:
降级策略:
user_id % 1024
),分为16库×64表。// 订单ID结构: 时间戳(41bit) + 分库编号(10bit) + 分表编号(6bit) + 序列号(7bit)
long orderId = (timestamp << 23) | (dbNo << 16) | (tableNo << 10) | sequence;
中间件 | 优势 | 适用场景 |
---|---|---|
ShardingSphere | 兼容性强,支持多种数据库 | 需要灵活路由规则的业务 |
MyCAT | 成熟稳定,社区活跃 | 传统分库分表改造项目 |
Vitess | Kubernetes原生,适合云环境 | 大规模MySQL集群管理 |
buy.example.com
)与常规业务分离,独立部署负载均衡。商品抢购阶段的架构设计需围绕三大核心展开:
在订单支付阶段,确保订单状态更新与异步通知的可靠性是核心挑战。要实现可靠消息投递机制的完整解决方案,可以结合本地消息表、幂等性设计与容错策略
本地消息表与事务绑定:
将订单状态更新与消息记录插入放在同一数据库事务中,保证原子性。
BEGIN TRANSACTION;
UPDATE orders SET status = 'paid' WHERE order_id = '1001';
INSERT INTO message_table (msg_id, order_id, status)
VALUES ('msg_001', '1001', 'pending');
COMMIT;
异步消息发送:
使用独立线程池或定时任务扫描本地消息表,将status=pending
的消息发送到MQ。
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<Message> messages = messageDao.selectPending();
for (Message msg : messages) {
mqProducer.send(msg);
messageDao.updateStatus(msg.getId(), "sent");
}
}
消息重试机制:
若MQ发送失败,通过指数退避策略重试(如首次1秒,第二次2秒,第三次4秒)。
唯一标识:为每条消息生成全局唯一ID(如msg_id
),下游服务通过该ID判断是否已处理。
public void handleMessage(Message msg) {
if (redis.setnx("msg_dedup:" + msg.getId(), "1")) {
addPoints(msg.getUserId(), msg.getPoints());
}
}
业务状态校验:
处理消息前检查业务状态(如积分是否已到账)。
SELECT * FROM user_points WHERE order_id = '1001';
-- 若存在记录,则跳过处理
public void handlePaymentCallback(String orderId) {
Order order = orderDao.select(orderId);
if (order.getStatus().equals("paid")) {
return; // 已处理,直接返回
}
// 处理支付逻辑
}
status=sent
但未ACK的消息,重新发送。enable.auto.commit=false
)。消息批量发送:合并多条消息为一批发送,减少MQ调用次数。
List<Message> batch = messages.subList(0, 100);
mqProducer.sendBatch(batch);
批量更新状态:
发送成功后批量更新消息状态为sent
。
UPDATE message_table SET status = 'sent'
WHERE msg_id IN ('msg_001', 'msg_002', ...);
replication.factor=3
,min.insync.replicas=2
。consumer_lag
)。// 使用Sentinel熔断
@SentinelResource(
value = "addPoints",
fallback = "addPointsFallback",
blockHandler = "addPointsBlockHandler"
)
public void addPoints(String userId, int points) { ... }
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
本地消息表 | 强一致性,无外部依赖 | 数据库压力大 | 中小规模业务 |
RocketMQ事务消息 | 无侵入,天然支持分布式事务 | 依赖特定MQ,成本高 | 高并发、强一致性场景 |
最大努力通知 | 实现简单,资源消耗低 | 可能丢失消息 | 容忍最终一致性的非核心业务 |
在订单支付阶段,通过本地消息表 + 异步重试 + 幂等性设计的组合方案,可有效解决支付回调与异步通知的可靠性问题。核心要点包括:
用户提交订单抢到商品后,此时系统的库存已经扣减掉了,但是订单中的状态还是未支付,如果此时用户是恶意的行为,只抢购不支付,那么怎么优化架构设计来应对这样的操作?
库存预占原子性:
使用Redis Lua脚本保证库存操作的原子性:
-- KEYS[1]=可售库存, KEYS[2]=预占库存
local available = tonumber(redis.call('GET', KEYS[1]))
if available <= 0 then
return 0
end
redis.call('DECR', KEYS[1])
redis.call('INCR', KEYS[2])
return 1
支付超时管理:
指标 | 检测规则 | 处置措施 |
---|---|---|
未支付订单率 | 用户近1小时未支付订单数 > 5 | 限制参与抢购1小时 |
设备指纹关联 | 同一设备生成 > 3个未支付订单 | 封禁设备ID |
IP异常请求 | 单个IP来源的抢购请求频率 > 100次/分钟 | IP限流或临时封禁 |
INCR user:1001:unpaid_orders
)。倒计时同步:
前端定时从服务端同步剩余时间(避免客户端时间篡改):
function syncCountdown() {
fetch('/api/payment/timeleft?orderId=1001')
.then(res => res.json())
.then(data => {
updateUI(data.remainingSeconds);
});
}
// 每10秒同步一次
setInterval(syncCountdown, 10000);
多通道提醒:
alipay.trade.precreate
),缩短支付跳转时间。-- 补偿任务SQL(伪代码)
UPDATE inventory
SET available = available + reserved,
reserved = 0
WHERE item_id IN (
SELECT item_id
FROM orders
WHERE status = 'unpaid'
AND create_time < NOW() - INTERVAL 30 MINUTE
);
指标 | 监控方式 | 告警阈值 |
---|---|---|
预占库存释放延迟 | Prometheus + Grafana | > 5分钟未释放 |
未支付订单占比 | ELK日志分析 | 超过10%触发预警 |
风控规则命中率 | 实时Dashboard | 单规则命中率突增50% |
通过预占库存机制 + 支付超时释放 + 实时风控拦截的组合方案,可有效解决恶意占库存问题: