在秒杀场景中,用户点击“抢购”后,后端需要通过异步处理应对高并发(避免请求阻塞),同时需通过实时回调机制将最终结果(成功/失败)推送给客户端并展示。核心方案是:“前端发起请求→后端生成唯一标识并异步处理→客户端通过轮询/长连接监听结果→后端处理完成后更新状态→客户端获取结果并展示”。
一、整体流程设计
秒杀场景的回调交互流程需满足:高并发下的请求不阻塞、结果通知及时、前端体验流畅,具体步骤如下:
- 用户触发抢购:前端点击按钮,发送抢购请求(携带用户ID、商品ID)。
- 后端接收请求:
- 生成唯一的请求ID(如UUID),作为本次秒杀的标识。
- 立即返回“请求已受理”给前端(避免用户等待),同时返回请求ID。
- 将秒杀任务(商品ID、用户ID、请求ID)放入消息队列(如RabbitMQ/Kafka),异步处理(库存检查、下单、扣减等)。
- 异步处理秒杀逻辑:后端消费者从消息队列取任务,执行秒杀(判断库存、防重复下单、创建订单等),处理完成后将结果(成功/失败原因)存入缓存(如Redis),关联请求ID。
- 客户端监听结果:前端拿到请求ID后,通过轮询/长轮询/WebSocket持续查询该ID的处理结果。
- 展示结果:一旦缓存中该请求ID的结果就绪,前端立即更新页面(如“抢购成功!订单号XXX”或“手慢了,商品已抢完”)。
整个流程涉及客户端(浏览器)、后端API、消息队列和业务Worker的协同工作。
二、核心技术方案与代码示例
以下以 Spring Boot(后端)+ Vue(前端) 为例,实现完整流程:
1. 后端实现(异步处理+结果缓存)
(1)定义请求ID与结果存储(Redis)
用Redis存储秒杀请求的状态,key为seckill:result:{请求ID},value为JSON格式的结果(如{"success":true, "orderId":"12345", "msg":""})。
(2)接收抢购请求,生成任务并返回请求ID
// 秒杀Controller
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private StringRedisTemplate redisTemplate;
// 用户点击抢购时调用
@PostMapping("/submit")
public Result submitSeckill(@RequestBody SeckillRequest request) {
// 1. 生成唯一请求ID
String requestId = UUID.randomUUID().toString();
// 2. 初始状态:处理中
redisTemplate.opsForValue().set(
"seckill:result:" + requestId,
"{\"success\":false, \"status\":\"processing\"}",
30, TimeUnit.MINUTES // 过期时间:30分钟
);
// 3. 发送任务到消息队列(异步处理)
SeckillTask task = new SeckillTask();
task.setRequestId(requestId);
task.setUserId(request.getUserId());
task.setGoodsId(request.getGoodsId());
rabbitTemplate.convertAndSend("seckill-exchange", "seckill.key", task);
// 4. 立即返回,告知客户端请求ID
return Result.success("请求已受理,请等待结果", requestId);
}
// 客户端查询结果的接口(供轮询/长轮询调用)
@GetMapping("/result/{requestId}")
public Result getResult(@PathVariable String requestId) {
String resultJson = redisTemplate.opsForValue().get("seckill:result:" + requestId);
if (resultJson == null) {
return Result.fail("请求ID不存在");
}
return Result.success(JSON.parseObject(resultJson));
}
}
// 秒杀请求参数
@Data
class SeckillRequest {
private Long userId;
private Long goodsId;
}
// 消息队列任务
@Data
class SeckillTask implements Serializable {
private String requestId;
private Long userId;
private Long goodsId;
}
(3)异步处理秒杀逻辑(消息队列消费者)
// 秒杀任务消费者
@Component
public class SeckillConsumer {
@Autowired
private SeckillService seckillService;
@Autowired
private StringRedisTemplate redisTemplate;
@RabbitListener(queues = "seckill-queue")
public void handleSeckill(SeckillTask task) {
String requestId = task.getRequestId();
try {
// 执行秒杀逻辑(检查库存、创建订单等)
SeckillResult result = seckillService.doSeckill(task.getUserId(), task.getGoodsId());
// 3. 更新Redis结果
String resultJson = JSON.toJSONString(result);
redisTemplate.opsForValue().set(
"seckill:result:" + requestId,
resultJson,
30, TimeUnit.MINUTES
);
} catch (Exception e) {
// 处理异常(如库存不足、重复下单)
String errorJson = JSON.toJSONString(new SeckillResult(false, null, e.getMessage()));
redisTemplate.opsForValue().set(
"seckill:result:" + requestId,
errorJson,
30, TimeUnit.MINUTES
);
}
}
}
// 秒杀结果封装
@Data
class SeckillResult {
private boolean success; // 是否成功
private String orderId; // 订单号(成功时返回)
private String msg; // 提示信息(失败时返回)
public SeckillResult(boolean success, String orderId, String msg) {
this.success = success;
this.orderId = orderId;
this.msg = msg;
}
}
2. 前端实现(轮询监听+结果展示)
前端拿到请求ID后,通过短轮询(适合中小流量)或WebSocket(适合高并发实时性要求高的场景)监听结果,这里以短轮询为例:
<template>
<div class="seckill-page">
<button @click="handleSeckill" v-if="!isProcessing">立即抢购</button>
<div v-if="isProcessing">
<div>正在抢购中,请稍候...</div>
<div class="loading"></div>
</div>
<div v-if="showResult" class="result">
<div v-if="result.success">
✅ 抢购成功!订单号:{{ result.orderId }}
</div>
<div v-else>
❌ 抢购失败:{{ result.msg }}
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
isProcessing: false, // 是否正在处理
showResult: false, // 是否显示结果
result: {}, // 秒杀结果
pollTimer: null // 轮询定时器
};
},
methods: {
// 点击抢购按钮
async handleSeckill() {
this.isProcessing = true;
this.showResult = false;
try {
// 1. 发送抢购请求,获取requestId
const res = await axios.post('/seckill/submit', {
userId: this.$store.state.userId,
goodsId: 1001 // 商品ID(示例)
});
const requestId = res.data.data;
// 2. 启动轮询,查询结果
this.startPolling(requestId);
} catch (e) {
this.isProcessing = false;
alert('请求失败,请重试');
}
},
// 轮询查询结果(每1-2秒一次,避免频繁请求)
startPolling(requestId) {
this.pollTimer = setInterval(async () => {
try {
const res = await axios.get(`/seckill/result/${requestId}`);
const result = res.data.data;
// 3. 判断是否处理完成(状态不是processing)
if (result.status !== 'processing') {
clearInterval(this.pollTimer);
this.isProcessing = false;
this.showResult = true;
this.result = result;
}
} catch (e) {
console.error('查询结果失败', e);
}
}, 1500); // 1.5秒轮询一次
}
},
beforeDestroy() {
// 组件销毁时清除定时器
if (this.pollTimer) {
clearInterval(this.pollTimer);
}
}
};
</script>
三、优化方案(应对高并发)
-
用长轮询替代短轮询:
短轮询会频繁发送请求,增加服务器压力。长轮询的逻辑是:客户端发起请求后,服务器hold住连接(30秒内),若期间结果就绪则立即返回;若超时未就绪,客户端再发起新请求。减少无效请求次数。// 长轮询版本的getResult接口 @GetMapping("/result/{requestId}") public DeferredResult<Result> getResultLongPolling(@PathVariable String requestId) { DeferredResult<Result> deferredResult = new DeferredResult<>(30000L); // 30秒超时 // 1. 检查当前结果是否就绪 String resultJson = redisTemplate.opsForValue().get("seckill:result:" + requestId); if (resultJson != null && !resultJson.contains("processing")) { deferredResult.setResult(Result.success(JSON.parseObject(resultJson))); return deferredResult; } // 2. 未就绪,注册监听器(当结果更新时触发) RedisMessageListenerContainer container = new RedisMessageListenerContainer(); // 监听Redis的key变化(需配置Redis监听) container.addMessageListener((message, pattern) -> { String updatedJson = message.toString(); deferredResult.setResult(Result.success(JSON.parseObject(updatedJson))); }, new PatternTopic("seckill:result:" + requestId)); // 3. 超时处理 deferredResult.onTimeout(() -> { deferredResult.setResult(Result.success("{\"status\":\"processing\"}")); }); return deferredResult; } -
WebSocket实时推送:
对于高并发场景(如百万级用户),可使用WebSocket建立长连接,后端处理完成后主动推送结果给客户端(无需客户端轮询)。
示例技术栈:后端用Spring WebSocket,前端用WebSocket API,通过请求ID关联用户连接。 -
限流与降级:
秒杀请求入口需限流(如用Redis+Lua脚本控制每秒请求量),避免后端被冲垮;同时,轮询接口也需限流(如限制单用户每秒最多2次请求)。
四、核心要点总结
- 异步处理:通过消息队列 decouple 秒杀请求的接收与处理,避免阻塞。
- 结果缓存:用Redis存储请求状态,支持高并发查询。
- 回调机制:根据流量规模选择轮询/长轮询/WebSocket,确保结果及时通知。
- 用户体验:前端需显示“处理中”状态,避免用户重复点击;结果页清晰反馈成功/失败原因。
通过这套方案,既能应对秒杀的高并发压力,又能通过回调机制实时将结果反馈给用户,平衡了性能与体验。
182

被折叠的 条评论
为什么被折叠?



