在秒杀场景中,用户点击“抢购”后,后端需要通过异步处理应对高并发(避免请求阻塞),同时需通过实时回调机制将最终结果(成功/失败)推送给客户端并展示。核心方案是:“前端发起请求→后端生成唯一标识并异步处理→客户端通过轮询/长连接监听结果→后端处理完成后更新状态→客户端获取结果并展示”。
一、整体流程设计
秒杀场景的回调交互流程需满足:高并发下的请求不阻塞、结果通知及时、前端体验流畅,具体步骤如下:
- 用户触发抢购:前端点击按钮,发送抢购请求(携带用户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 {@Autowiredprivate RabbitTemplate rabbitTemplate;@Autowiredprivate StringRedisTemplate redisTemplate;// 用户点击抢购时调用@PostMapping("/submit")public Result submitSeckill(@RequestBody SeckillRequest request) {// 1. 生成唯一请求IDString 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. 立即返回,告知客户端请求IDreturn 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 {@Autowiredprivate SeckillService seckillService;@Autowiredprivate 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. 发送抢购请求,获取requestIdconst 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,确保结果及时通知。
- 用户体验:前端需显示“处理中”状态,避免用户重复点击;结果页清晰反馈成功/失败原因。
通过这套方案,既能应对秒杀的高并发压力,又能通过回调机制实时将结果反馈给用户,平衡了性能与体验。