秒杀服务的回调方案

在秒杀场景中,用户点击“抢购”后,后端需要通过异步处理应对高并发(避免请求阻塞),同时需通过实时回调机制将最终结果(成功/失败)推送给客户端并展示。核心方案是:“前端发起请求→后端生成唯一标识并异步处理→客户端通过轮询/长连接监听结果→后端处理完成后更新状态→客户端获取结果并展示”

一、整体流程设计

秒杀场景的回调交互流程需满足:高并发下的请求不阻塞、结果通知及时、前端体验流畅,具体步骤如下:

  1. 用户触发抢购:前端点击按钮,发送抢购请求(携带用户ID、商品ID)。
  2. 后端接收请求
    • 生成唯一的请求ID(如UUID),作为本次秒杀的标识。
    • 立即返回“请求已受理”给前端(避免用户等待),同时返回请求ID。
    • 将秒杀任务(商品ID、用户ID、请求ID)放入消息队列(如RabbitMQ/Kafka),异步处理(库存检查、下单、扣减等)。
  3. 异步处理秒杀逻辑:后端消费者从消息队列取任务,执行秒杀(判断库存、防重复下单、创建订单等),处理完成后将结果(成功/失败原因)存入缓存(如Redis),关联请求ID。
  4. 客户端监听结果:前端拿到请求ID后,通过轮询/长轮询/WebSocket持续查询该ID的处理结果。
  5. 展示结果:一旦缓存中该请求ID的结果就绪,前端立即更新页面(如“抢购成功!订单号XXX”或“手慢了,商品已抢完”)。

整个流程涉及客户端(浏览器)后端API消息队列业务Worker的协同工作。

用户/浏览器客户端页面后端API网关消息队列业务Worker回调服务/WS连接数据库/缓存点击“立即抢购”发送抢购请求 (req_id, user_id)同步请求预检过滤(风控、是否已抢过)发送异步任务消息 (req_id, user_id, ...)202 Accepted, “请求已接受,处理中”显示“排队中,请稍候...”异步处理拉取任务消息执行核心事务:读库存、校验、扣库存、创建订单更新处理状态(成功/失败)回调通知推送处理结果 (req_id, result)通过WS/LP/Push将结果推送到浏览器页面更新显示最终结果:“成功”或“失败”用户/浏览器客户端页面后端API网关消息队列业务Worker回调服务/WS连接数据库/缓存

二、核心技术方案与代码示例

以下以 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>

三、优化方案(应对高并发)

  1. 用长轮询替代短轮询
    短轮询会频繁发送请求,增加服务器压力。长轮询的逻辑是:客户端发起请求后,服务器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;
    }
    
  2. WebSocket实时推送
    对于高并发场景(如百万级用户),可使用WebSocket建立长连接,后端处理完成后主动推送结果给客户端(无需客户端轮询)。
    示例技术栈:后端用Spring WebSocket,前端用WebSocket API,通过请求ID关联用户连接。

  3. 限流与降级
    秒杀请求入口需限流(如用Redis+Lua脚本控制每秒请求量),避免后端被冲垮;同时,轮询接口也需限流(如限制单用户每秒最多2次请求)。

四、核心要点总结

  1. 异步处理:通过消息队列 decouple 秒杀请求的接收与处理,避免阻塞。
  2. 结果缓存:用Redis存储请求状态,支持高并发查询。
  3. 回调机制:根据流量规模选择轮询/长轮询/WebSocket,确保结果及时通知。
  4. 用户体验:前端需显示“处理中”状态,避免用户重复点击;结果页清晰反馈成功/失败原因。

通过这套方案,既能应对秒杀的高并发压力,又能通过回调机制实时将结果反馈给用户,平衡了性能与体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值