文章目录
- 案例:预约抢茅子
- 复杂度分析
- 商品预约阶段
- 等待抢购阶段
- 商品抢购阶段
- 订单支付阶段
- 技术方案
- 商品预约阶段
- 一、基于 Redis 单节点的分布式锁方案
- 1. 核心流程
- 2. 关键设计点
- 二、Redis 单节点方案的局限性
- 1. 单点故障风险
- 2. 主从切换问题
- 三、多节点 Redis 实现高可靠分布式锁(RedLock)
- 1. RedLock 核心流程
- 2. RedLock 关键设计
- 3. RedLock 的争议与改进
- 四、不同场景下的技术选型
- 五、补充优化措施
- 六、小结
- 等待抢购阶段
- 一、页面静态化:降低服务端负载
- 1. 静态资源拆分与缓存
- 2. CDN缓存策略优化
- 3. 动态内容降级方案
- 二、服务端限流:保护核心系统
- 1. 多层级限流设计
- 2. Nginx限流配置示例
- 3. 用户体验优化
- 三、动态内容优化:降低API压力
- 1. 客户端长轮询替代短轮询
- 2. 边缘计算缓存动态数据
- 四、方案效果与权衡
- 五、小结
- 商品抢购阶段
- 一、流量削峰:异步化处理与消息队列优化
- 1. 核心流程设计
- 2. 消息队列
- 二、扣减库存:高并发下的数据一致性保障
- 1. Redis集群方案
- 2. 可靠性增强
- 三、分库分表:订单存储的高并发支持
- 1. 分片策略
- 2. 中间件选型
- 四、带宽与网络优化
- 1. 独立域名与DNS
- 2. 协议优化
- 3. 边缘计算
- 五、小结
- 订单支付阶段
- 一、支付回调阶段的可靠消息投递设计
- 1. 整体流程
- 2. 关键设计
- 3. 消息消费的幂等性
- 二、容错与异常处理
- 1. 支付回调接口的幂等性
- 2. 消息补偿机制
- 3. 数据库与MQ的一致性
- 三、性能优化
- 1. 本地消息表设计优化
- 2. 批量处理
- 3. 异步化处理
- 四、高可用与监控
- 1. 消息队列高可用
- 2. 监控告警
- 3. 熔断降级
- 五、方案对比与选型
- 小结
- 抢购后不支付导致的库存占用问题
- 一、预占库存与支付超时释放机制
- 1. 库存状态分层设计
- 2. 预占库存实现流程
- 3. 关键技术实现
- 二、恶意用户识别与拦截
- 1. 用户行为风控模型
- 2. 实时风控系统架构
- 三、支付倒计时与用户提醒
- 1. 前端体验优化
- 2. 支付链路优化
- 四、数据一致性保障
- 1. 最终一致性方案
- 2. 分布式事务
- 五、监控与容灾
- 1. 核心监控指标
- 2. 熔断降级策略
- 小结
案例:预约抢茅子
业务流程如下:
-
商品预约:用户进入商品详情页面,获取购买资格,并等待商品抢购倒计时。
-
等待抢购:等待商品抢购倒计时,直到商品开放抢购。
-
商品抢购:商品抢购倒计时结束,用户提交抢购订单,排队等待抢购结果,抢购成功后,扣减系统库存,生成抢购订单。
-
订单支付:等待用户支付成功后,系统更新订单状态,通知用户购买成功。
复杂度分析
根据不同的业务流程阶段,逐一分析一下每个环节可能存在的技术挑战
商品预约阶段
在高并发量的情况下,让每个用户都能得到抢购资格 ?
等待抢购阶段
用户预约成功之后,在商品详情页面中,会存在一个抢购倒计时,这个倒计时的初始时间是从服务端获取的,用户点击购买按钮时,系统还会去服务端验证是否已经到了抢购时间。
在等待抢购阶段,流量突增,因为在抢购商品之前(尤其是临近开始抢购之前的一分钟内),大部分用户会频繁刷新商品详情页,商品详情页面的读请求量剧增, 如果商品详情页面没有做好流量控制,就容易成为整个预约抢购系统中的性能瓶颈点
商品抢购阶段
在商品抢购阶段,用户会点击提交订单,这时,抢购系统会先校验库存,当库存足够时,系统会先扣减库存,然后再生成订单。在这个过程中,短时间之内提交订单的写流量非常高
订单支付阶段
在用户支付订单完成之后,一般会由支付平台回调系统接口,更新订单状态。在支付回调成功之后,抢购系统还会通过异步通知的方式,实现订单更新之外的非核心业务处理,比如积分累计、短信通知等
技术方案
商品预约阶段
在商品预约阶段中,高并发场景下需要保证用户预约资格的公平性和可靠性,同时允许预约量超过实际库存。
在商品预约阶段中,高并发场景下需要保证用户预约资格的公平性和可靠性,同时允许预约量超过实际库存。以下是基于分布式锁技术(参考第06讲内容)的完整技术分析,以及针对 Redis 单点故障问题的补充解决方案:
一、基于 Redis 单节点的分布式锁方案
1. 核心流程
// 伪代码示例:用户预约资格发放
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));
}
}
2. 关键设计点
- 锁粒度:按商品ID分片锁(
reserve_lock:itemId
),避免全局锁竞争。 - 防重复预约:使用 Redis 集合(
sadd/sismember
)记录已预约用户。 - 超量预约支持:通过
incr
统计预约量,不依赖实际库存限制。 - 锁超时机制:设置 10s 锁超时(
PX 10000
),避免客户端异常导致死锁。
二、Redis 单节点方案的局限性
1. 单点故障风险
- 问题:若 Redis 主节点宕机,锁数据丢失,导致业务中断。
- 表现:
- 新客户端无法获取锁(锁变量丢失)。
- 已持有锁的客户端误认为锁仍有效(但锁实际已丢失)。
2. 主从切换问题
- 若使用 Redis 主从集群,在主节点宕机后,由于 Redis 主从复制是异步的:
- 客户端A在主节点获取锁。
- 主节点宕机,从节点升级为新主节点,但未同步锁数据。
- 客户端B在新主节点也能获取同一把锁,导致锁互斥性失效。
三、多节点 Redis 实现高可靠分布式锁(RedLock)
针对单点故障问题,需引入 RedLock 算法(参考第06讲),基于多个独立 Redis 节点实现分布式锁。
1. RedLock 核心流程
// 伪代码: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;
}
}
2. RedLock 关键设计
设计点 | 说明 |
---|---|
独立节点部署 | 使用至少5个独立的 Redis 主节点(非集群模式),降低同时故障概率 |
时钟同步要求 | 各节点需使用 NTP 服务保证时钟同步,避免锁过期时间计算偏差 |
锁有效期补偿 | 锁实际有效时间 = 初始 TTL - 加锁耗时,防止因网络延迟导致锁提前失效 |
失败回滚机制 | 加锁失败时需异步释放已获得的锁,避免残留锁数据 |
3. RedLock 的争议与改进
- 争议点(Martin Kleppmann):
- 依赖系统时钟同步,时钟跳跃可能导致锁失效。
- 锁的有效期难以精确计算,存在理论上的竞态条件。
- 改进方案:
- 使用 fencing token(递增令牌)机制,确保锁释放后旧请求不生效。
- 结合业务层幂等性设计,容忍极低概率的锁失效问题。
四、不同场景下的技术选型
场景 | 技术方案 | 优缺点 |
---|---|---|
预约量较小(QPS < 1万) | 单 Redis 节点 + 哨兵模式 | 实现简单,但主从切换时存在短暂不可用 |
高可靠性要求(金融场景) | RedLock + 5个独立 Redis 节点 | 高可用,但实现复杂、性能较低(需多节点交互) |
超高性能要求(QPS > 10万) | Redis 集群 + 细粒度锁(按用户ID分片) | 通过分片提升并发能力,但需解决数据倾斜问题 |
五、补充优化措施
-
无锁化设计尝试:
- 使用 Redis
INCR
原子操作统计预约总数,替代分布式锁。 - 通过
SETNX user_reserved:userId:itemId 1
实现用户级防重复预约。 - 优点:性能更高;缺点:无法处理跨多键的原子操作。
- 使用 Redis
-
熔断降级策略:
- 当 Redis 不可用时,降级到本地限流(如令牌桶算法),保障核心流程可用。
- 记录预约请求到 Kafka 队列,后台异步补偿处理。
-
监控与告警:
- 监控 Redis 锁等待时间、锁竞争频率。
- 设置锁持有超时告警(如锁持有时间 > 8s 时触发)。
六、小结
在商品预约阶段,通过分布式锁控制资格发放时需综合考虑:
- 锁的可靠性:单节点方案简单但存在单点故障,RedLock 更可靠但实现复杂。
- 性能与一致性平衡:根据业务容忍度选择最终一致性或强一致性方案。
- 容灾设计:结合熔断降级、异步补偿机制应对极端场景。
实际工程中,若允许极低概率的重复预约,可优先使用单 Redis 节点 + 哨兵模式;若需强一致,则选择 RedLock + fencing token 组合方案。
等待抢购阶段
在等待抢购阶段,应对流量突增问题需综合运用页面静态化与服务端限流策略,并结合动态内容优化与用户体验设计。
一、页面静态化:降低服务端负载
1. 静态资源拆分与缓存
- 核心原则:将商品详情页中不变的内容(如商品图片、描述、规格参数)与动态内容(倒计时、库存状态)分离。
- 实现步骤:
- 生成静态HTML:使用模板引擎(如Thymeleaf、Velocity)在抢购开始前预生成静态页面,存储至CDN。
- 动态内容异步加载:在静态页面中通过JavaScript调用API获取倒计时、抢购状态等动态数据。
<!-- 静态页面示例 --> <html> <body> <div id="product-image"><!-- 静态图片URL --></div> <div id="countdown-timer"></div> <script> // 异步获取倒计时 fetch('/api/countdown?itemId=1001') .then(response => response.json()) .then(data => updateCountdown(data)); </script> </body> </html>
2. CDN缓存策略优化
- 缓存规则:
- 静态资源(HTML、CSS、JS、图片):设置长期缓存(如1年),通过文件名哈希(
main.[hash].css
)实现版本更新。 - 动态API响应:在CDN边缘节点缓存倒计时接口(
/api/countdown
),设置短时间缓存(如1秒),确保用户看到准实时数据。
- 静态资源(HTML、CSS、JS、图片):设置长期缓存(如1年),通过文件名哈希(
- 预热与刷新:
- 预缓存:在抢购开始前1小时,通过CDN预热工具(如阿里云CDN Prefetch)主动加载静态资源至所有节点。
- 强制刷新:抢购开始时,通过CDN API刷新关键页面(如商品详情页),确保用户获取最新版本。
3. 动态内容降级方案
- 本地时钟兜底:若倒计时API不可用,前端使用本地时钟计算剩余时间,并在倒计时结束后提示用户刷新页面。
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); } }
二、服务端限流:保护核心系统
1. 多层级限流设计
层级 | 限流策略 | 工具/实现 |
---|---|---|
CDN/边缘节点 | 限制同一IP的请求频率(如10次/秒) | 阿里云CDN频率控制、Cloudflare Rate Limiting |
网关层 | 按API维度限制QPS(如商品详情页动态接口10万QPS) | Nginx limit_req (漏桶算法)、Spring Cloud Gateway RequestRateLimiter |
应用层 | 基于用户ID或设备指纹的细粒度限流(如单个用户每秒最多5次请求) | Redis + Lua脚本(计数器算法) |
服务层 | 熔断非核心服务(如推荐系统、用户画像),保障抢购链路资源 | Hystrix、Sentinel |
2. Nginx限流配置示例
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
:突发请求不延迟处理,直接拒绝超限请求。
3. 用户体验优化
- 排队机制:前端在限流响应(HTTP 429)时展示排队等待页,并自动重试。
// 前端处理限流响应 fetch('/api/countdown') .catch(error => { if (error.status === 429) { showQueuePage(); // 显示排队页,倒计时后重试 } });
- 请求退避策略:前端在重试时采用指数退避(Exponential Backoff),减少服务端压力。
let retries = 0; function fetchWithRetry() { fetch('/api/countdown') .catch(() => { setTimeout(() => { retries++; fetchWithRetry(); }, Math.min(1000 * 2 ** retries, 30000)); }); }
三、动态内容优化:降低API压力
1. 客户端长轮询替代短轮询
- 方案:使用长轮询(Long Polling)或WebSocket减少无效请求。
// 长轮询示例 function longPollCountdown() { fetch('/api/countdown?longPoll=true') .then(response => { updateCountdown(response.data); longPollCountdown(); // 递归调用 }) .catch(() => setTimeout(longPollCountdown, 5000)); }
- 服务端实现:当倒计时未结束时,服务端挂起请求,直到时间临近(如剩余10秒)再响应。
2. 边缘计算缓存动态数据
- 方案:将倒计时数据缓存在CDN边缘节点。
// 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费用低 |
五、小结
等待抢购阶段的流量突增问题需通过动静分离与分层限流综合解决:
- 静态化:将页面主体内容缓存在CDN,减少回源请求。
- 限流:在网关层拦截超量请求,保护后端服务。
- 动态内容优化:使用长轮询、边缘缓存降低API压力。
- 容灾与监控:通过自动扩缩容和混沌工程保障系统韧性。
商品抢购阶段
在商品抢购阶段,面对瞬时高并发流量,需通过多层次架构设计保障系统的高可用性与数据一致性。主要依赖流量削峰、扣减库存、分库分表三大核心方案。
一、流量削峰:异步化处理与消息队列优化
1. 核心流程设计
2. 消息队列
-
防消息丢失:
- 生产者确认:启用RabbitMQ的
publisher confirms
或Kafka的acks=all
。 - 持久化存储:消息持久化到磁盘,副本数≥3(Kafka推荐配置)。
- 消费者手动ACK:业务处理成功后再提交偏移量。
- 生产者确认:启用RabbitMQ的
-
消息积压处理:
- 动态扩缩容:基于队列长度自动增加消费者实例(如Kubernetes HPA)。
- 批量消费:单次拉取多条消息(Kafka的
max.poll.records=500
)。 - 死信队列:处理失败消息,避免阻塞正常流程。
-
消息去重:
- 唯一业务ID:订单ID使用雪花算法生成,Redis记录已处理ID。
String orderId = "ORDER_" + snowflake.nextId(); if (redis.setnx("order:dedup:" + orderId, "1") == 1) { processOrder(orderId); }
二、扣减库存:高并发下的数据一致性保障
1. Redis集群方案
-
架构选择:
- Redis Cluster:自动分片(16384 slots),支持水平扩展。
- Codis:Proxy-based分片方案,适合大规模集群。
-
库存扣减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
2. 可靠性增强
-
多级缓存兜底:
- 本地缓存:Guava Cache记录热点商品库存(短时间缓存,如1秒)。
- 数据库异步校对:定时任务对比Redis与数据库库存,修复差异。
-
降级策略:
- 限流降级:库存不足时直接返回失败,避免无效请求穿透。
- 预扣库存:提前将库存从数据库加载至Redis,避免击穿。
三、分库分表:订单存储的高并发支持
1. 分片策略
- 分片键选择:以用户ID后4位取模(
user_id % 1024
),分为16库×64表。 - 基因法分片:将分片信息嵌入订单ID,避免跨库查询。
// 订单ID结构: 时间戳(41bit) + 分库编号(10bit) + 分表编号(6bit) + 序列号(7bit) long orderId = (timestamp << 23) | (dbNo << 16) | (tableNo << 10) | sequence;
2. 中间件选型
中间件 | 优势 | 适用场景 |
---|---|---|
ShardingSphere | 兼容性强,支持多种数据库 | 需要灵活路由规则的业务 |
MyCAT | 成熟稳定,社区活跃 | 传统分库分表改造项目 |
Vitess | Kubernetes原生,适合云环境 | 大规模MySQL集群管理 |
四、带宽与网络优化
1. 独立域名与DNS
- 子域名隔离:将抢购接口(如
buy.example.com
)与常规业务分离,独立部署负载均衡。 - DNS负载均衡:配置加权轮询(WRR)或基于地理位置的DNS解析。
2. 协议优化
- HTTP/2多路复用:减少TCP连接数,提升传输效率。
- QUIC协议:Google推出的基于UDP的低延迟协议,适用于高并发场景。
3. 边缘计算
- CDN动态加速:通过边缘节点转发API请求,减少网络延迟。
- WebSocket长连接:保持用户会话,减少重复握手开销。
五、小结
商品抢购阶段的架构设计需围绕三大核心展开:
- 流量削峰:通过异步队列与动态扩缩容应对瞬时高峰。
- 扣减库存:基于Redis集群与原子操作保障高并发下的数据一致性。
- 分库分表:结合基因分片与柔性事务实现海量订单存储。
订单支付阶段
在订单支付阶段,确保订单状态更新与异步通知的可靠性是核心挑战。要实现可靠消息投递机制的完整解决方案,可以结合本地消息表、幂等性设计与容错策略
一、支付回调阶段的可靠消息投递设计
1. 整体流程
2. 关键设计
-
本地消息表与事务绑定:
将订单状态更新与消息记录插入放在同一数据库事务中,保证原子性。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秒)。
3. 消息消费的幂等性
-
唯一标识:为每条消息生成全局唯一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'; -- 若存在记录,则跳过处理
二、容错与异常处理
1. 支付回调接口的幂等性
- 设计要点:
- 支付平台可能多次回调,需保证订单状态更新幂等。
- 在更新订单状态前先查询当前状态。
public void handlePaymentCallback(String orderId) { Order order = orderDao.select(orderId); if (order.getStatus().equals("paid")) { return; // 已处理,直接返回 } // 处理支付逻辑 }
2. 消息补偿机制
- 场景:消息发送失败或消费失败。
- 方案:
- 定时任务扫描:定期检查本地消息表中
status=sent
但未ACK的消息,重新发送。 - 死信队列(DLQ):MQ将多次重试失败的消息转入DLQ,触发人工干预。
- 定时任务扫描:定期检查本地消息表中
3. 数据库与MQ的一致性
- 最终一致性保障:
- 本地消息表:确保消息至少被发送一次。
- 消费者ACK机制:MQ在消息被成功处理后提交确认(如Kafka的
enable.auto.commit=false
)。
三、性能优化
1. 本地消息表设计优化
- 分库分表:按订单ID分片,避免单表过大。
- 读写分离:将消息表的查询操作路由到从库。
2. 批量处理
-
消息批量发送:合并多条消息为一批发送,减少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', ...);
3. 异步化处理
- 非阻塞IO:使用Netty或异步Servlet处理支付回调,避免线程阻塞。
- 线程池隔离:核心业务(订单状态更新)与非核心业务(消息发送)使用独立线程池。
四、高可用与监控
1. 消息队列高可用
- 集群部署:使用Kafka多副本机制,确保Broker故障时自动切换。
- 持久化配置:
Kafka设置replication.factor=3
,min.insync.replicas=2
。
2. 监控告警
- 关键指标:
- 消息积压量:MQ消费者Lag(如Kafka的
consumer_lag
)。 - 处理延迟:从消息生产到消费的时间差。
- 错误率:消息发送/消费失败的比例。
- 消息积压量:MQ消费者Lag(如Kafka的
3. 熔断降级
- 规则配置:当积分服务故障时,暂停消息消费并降级。
// 使用Sentinel熔断 @SentinelResource( value = "addPoints", fallback = "addPointsFallback", blockHandler = "addPointsBlockHandler" ) public void addPoints(String userId, int points) { ... }
五、方案对比与选型
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
本地消息表 | 强一致性,无外部依赖 | 数据库压力大 | 中小规模业务 |
RocketMQ事务消息 | 无侵入,天然支持分布式事务 | 依赖特定MQ,成本高 | 高并发、强一致性场景 |
最大努力通知 | 实现简单,资源消耗低 | 可能丢失消息 | 容忍最终一致性的非核心业务 |
小结
在订单支付阶段,通过本地消息表 + 异步重试 + 幂等性设计的组合方案,可有效解决支付回调与异步通知的可靠性问题。核心要点包括:
- 事务绑定:确保订单状态更新与消息记录原子性。
- 消息可靠投递:通过重试、ACK、死信队列实现最终一致性。
- 下游幂等:防止重复处理导致数据错误。
- 监控与容错:实时感知异常并触发补偿机制。
抢购后不支付导致的库存占用问题
用户提交订单抢到商品后,此时系统的库存已经扣减掉了,但是订单中的状态还是未支付,如果此时用户是恶意的行为,只抢购不支付,那么怎么优化架构设计来应对这样的操作?
一、预占库存与支付超时释放机制
1. 库存状态分层设计
- 可售库存:前端展示的剩余数量,用户可见。
- 预占库存:用户下单后临时占用的库存(支付超时后释放)。
- 已售库存:支付成功后永久扣除的库存。
2. 预占库存实现流程
3. 关键技术实现
-
库存预占原子性:
使用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
-
支付超时管理:
- 延迟队列:使用RabbitMQ死信队列(DLX)或RocketMQ延迟消息触发超时检查。
- 定时任务分片:按订单ID哈希分片,分布式调度(如ElasticJob)避免单点压力。
二、恶意用户识别与拦截
1. 用户行为风控模型
指标 | 检测规则 | 处置措施 |
---|---|---|
未支付订单率 | 用户近1小时未支付订单数 > 5 | 限制参与抢购1小时 |
设备指纹关联 | 同一设备生成 > 3个未支付订单 | 封禁设备ID |
IP异常请求 | 单个IP来源的抢购请求频率 > 100次/分钟 | IP限流或临时封禁 |
2. 实时风控系统架构
- 技术选型:
- 流计算:Apache Flink实时统计用户行为指标。
- 特征存储:Redis存储用户行为计数(如
INCR user:1001:unpaid_orders
)。 - 决策引擎:Drools规则引擎动态加载风控策略。
三、支付倒计时与用户提醒
1. 前端体验优化
-
倒计时同步:
前端定时从服务端同步剩余时间(避免客户端时间篡改):function syncCountdown() { fetch('/api/payment/timeleft?orderId=1001') .then(res => res.json()) .then(data => { updateUI(data.remainingSeconds); }); } // 每10秒同步一次 setInterval(syncCountdown, 10000);
-
多通道提醒:
- 站内信:用户登录时提示待支付订单。
- 短信/邮件:支付截止前15分钟发送提醒。
2. 支付链路优化
- 预创建支付单:在生成订单时预创建支付流水(如支付宝的
alipay.trade.precreate
),缩短支付跳转时间。 - 支付状态主动查询:前端轮询支付结果,避免依赖异步回调延迟。
四、数据一致性保障
1. 最终一致性方案
- 库存释放补偿任务:
定时扫描预占库存与订单状态,修复异常数据:-- 补偿任务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 );
2. 分布式事务
- Saga模式:
五、监控与容灾
1. 核心监控指标
指标 | 监控方式 | 告警阈值 |
---|---|---|
预占库存释放延迟 | Prometheus + Grafana | > 5分钟未释放 |
未支付订单占比 | ELK日志分析 | 超过10%触发预警 |
风控规则命中率 | 实时Dashboard | 单规则命中率突增50% |
2. 熔断降级策略
- 库存服务不可用:
降级为同步扣减数据库库存,事后通过对账修复。 - 风控服务超时:
跳过风控检查,记录日志事后审计。
小结
通过预占库存机制 + 支付超时释放 + 实时风控拦截的组合方案,可有效解决恶意占库存问题:
- 资源隔离:预占库存与实际可售库存分离,避免恶意占用影响正常销售。
- 及时释放:通过延迟队列和定时任务确保超时订单快速释放。
- 行为管控:实时风控识别恶意用户,降低资源浪费。
- 体验优化:倒计时提醒与支付链路优化,提升用户支付率。