撑住流量洪峰!七个 Java 高并发秒杀原则助你轻松扛住双11!

真正能撑住双 11 流量洪峰的,不是某个单一的优化,而是 从前端到数据库的全链路协同设计。当你构建出这样一套体系,面对千万级并发也能做到“系统稳如老狗”,再也不用担心用户抱怨“点了没反应,结果秒没了”。

想象这样一个场景:

  • 午夜零点,某款新手机开放抢购;
  • 数十万人几乎在同一毫秒同时点击“立即购买”;
  • 页面瞬间卡死,支付没反应,库存提示“售罄”;
  • 结果大多数人都没有下单成功,却感觉像被“欺骗”。

这就是 高并发秒杀。它不仅仅是卖货的活动,更是对 系统架构、并发处理、用户体验 的极限考验。

秒杀的本质特点包括:

  • 瞬时高并发:同时涌入的请求可能超过 10 万;
  • 资源极度稀缺:几百件商品要面对几十万的抢购请求;
  • 一致性要求高:不能出现超卖、重复下单;
  • 用户体验敏感:哪怕页面延迟一秒,都会带来大量投诉。

如果没有任何防护,所有请求直冲数据库,结局可想而知:连接池打满、锁竞争、CPU 飙升、事务阻塞、系统崩溃

那我们该如何从容应对?本文将通过 7 大策略,结合前后端代码实例,为你完整拆解 Java 高并发秒杀的应对方案。

一、前端优化:第一层拦截

前端虽然无法彻底阻止恶意攻击,但却是最接近用户的一层,可以显著减少无效请求涌入后端。

按钮防连点(防误触)

避免用户因手滑或卡顿多次提交请求。

<template>
  <button
    :disabled="isDisabled || countdown > 0"
    @click="handleClick"
    class="seckill-btn"
  >
    {{ buttonText }}
  </button>
</template>


<script>
export default {
  data() {
    return {
      isDisabled: false,
      countdown: 0,
      maxCountdown: 5
    }
  },
  computed: {
    buttonText() {
      if (this.countdown > 0) return `请等待${this.countdown}秒`
      return this.isDisabled ? '抢购中...' : '立即抢购'
    }
  },
  methods: {
    async handleClick() {
      if (this.isDisabled || this.countdown > 0) return
      this.isDisabled = true
      try {
        const result = await this.$api.createOrder()
        if (result.success) {
          this.$message.success('抢购成功')
        } else {
          this.startCountdown()
          this.$message.error(result.message || '抢购失败')
        }
      } catch (e) {
        this.startCountdown()
        this.$message.error('请求异常,请稍后重试')
      } finally {
        this.isDisabled = false
      }
    },
    startCountdown() {
      this.countdown = this.maxCountdown
      const timer = setInterval(() => {
        this.countdown--
        if (this.countdown <= 0) clearInterval(timer)
      }, 1000)
    }
  }
}
</script>

优化点:

  • 请求失败后,按钮进入冷却期,避免用户疯狂点击;
  • 提供友好的提示,降低用户焦虑感。
请求频率限制(节流)

即使前端无法防住所有脚本攻击,节流 依然是减少无效请求的有效手段。

function throttle(fn, delay) {
  let lastCall = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastCall < delay) {
      console.warn('操作过于频繁')
      return
    }
    lastCall = now
    return fn.apply(this, args)
  }
}


const throttledCreateOrder = throttle(createOrder, 500) // 500ms 内最多一次

二、后端核心方案:架构护城河

流量削峰:Redis Streams 缓冲队列

直接把上百万请求打到数据库,必死无疑。我们需要 Redis Streams 来作为缓冲器。

路径:/src/main/java/com/icoderoad/seckill/service/SeckillQueueService.java

package com.icoderoad.seckill.service;


@Slf4j
@Service
public class SeckillQueueService {


    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    private static final String STREAM_KEY = "seckill:requests";
    private static final String GROUP = "seckill_group";
    private static final String DLQ_KEY = "seckill:dlq";


    @PostConstruct
    public void initGroup() {
        try {
            redisTemplate.opsForStream().createGroup(STREAM_KEY, ReadOffset.from("0-0"), GROUP);
        } catch (Exception e) {
            log.info("消费者组已存在: {}", GROUP);
        }
    }


    /** 入队:防重复提交 + 写入消息队列 */
    public boolean enqueue(String userId, String productId) {
        String key = "seckill:participated:" + productId;
        if (Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, userId))) return false;


        Map<String, Object> msg = Map.of(
            "userId", userId,
            "productId", productId,
            "time", System.currentTimeMillis(),
            "reqId", UUID.randomUUID().toString()
        );


        redisTemplate.opsForStream().add(STREAM_KEY, msg);
        redisTemplate.opsForSet().add(key, userId);
        redisTemplate.expire(key, 10, TimeUnit.MINUTES);
        return true;
    }


    /** 消费请求 */
    @Async("seckillExecutor")
    public void consume() {
        while (true) {
            try {
                List<MapRecord<String, Object, Object>> records = redisTemplate.opsForStream()
                        .read(Consumer.from(GROUP, "c-" + Thread.currentThread().getId()),
                              StreamReadOptions.empty().count(1).block(Duration.ofSeconds(5)),
                              StreamOffset.create(STREAM_KEY, ReadOffset.lastConsumed()));


                if (records == null) continue;


                for (MapRecord<String, Object, Object> rec : records) {
                    Map<Object, Object> body = rec.getValue();
                    String uid = (String) body.get("userId");
                    String pid = (String) body.get("productId");


                    if (process(uid, pid)) {
                        redisTemplate.opsForStream().acknowledge(STREAM_KEY, GROUP, rec.getId());
                    } else {
                        redisTemplate.opsForList().leftPush(DLQ_KEY, body);
                    }
                }
            } catch (Exception e) {
                log.error("消费异常", e);
                try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
            }
        }
    }


    private boolean process(String uid, String pid) {
        if (reduceStock(pid, 1)) {
            createOrder(uid, pid);
            return true;
        }
        return false;
    }


    private boolean reduceStock(String pid, int count) {
        // Redis Lua 扣减逻辑
        return true;
    }


    private void createOrder(String uid, String pid) {
        log.info("订单创建成功:user={} product={}", uid, pid);
    }
}

优化点:

  • 使用 DLQ 死信队列保存失败请求,便于人工干预;
  • 异步消费,平滑处理请求;
  • 结合 Redis ACK 机制,保证消息可靠。

三、防超卖:Redis + Lua 原子扣减

路径:/src/main/java/com/icoderoad/seckill/service/StockService.java

核心逻辑:检查库存 + 扣减 必须是一个原子操作,否则会导致超卖。

@Service
@Slf4j
public class StockService {


    @Resource
    private RedisTemplate<String, Object> redisTemplate;


    private static final String LUA_SCRIPT =
        "local stock = redis.call('get', KEYS[1]) " +
        "if not stock then return 0 " +
        "elseif tonumber(stock) < tonumber(ARGV[1]) then return 0 " +
        "else redis.call('decrby', KEYS[1], ARGV[1]) return 1 end";


    public boolean reduceStock(String productId, int qty) {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
        Long res = redisTemplate.execute(script, List.of("product:stock:" + productId), String.valueOf(qty));
        return res != null && res == 1L;
    }


    public void preloadStock(String productId, int stock) {
        redisTemplate.opsForValue().set("product:stock:" + productId, stock);
    }


    public void syncStockToDB(String productId, int finalStock) {
        // 批量写回 MySQL,避免频繁 I/O
        log.info("库存同步至DB product={} stock={}", productId, finalStock);
    }
}

四、防重复下单:多层幂等控制

  • Redis 集合:快速拦截
  • 数据库唯一索引:最终保障
  • Token 机制:防机器人刷单
  • 分布式锁:防止并发写入

这里保留示例代码,优化在于增加日志与统一异常处理。

五、限流与降级:Sentinel

利用 Alibaba Sentinel,针对 QPS、参数热点、异常比例 实现全链路保护。

六、库存回滚:预扣 + 超时释放

防止“下单未支付”造成库存假性减少。

方案:

  • 抢购成功后先 预扣库存,写入 Redis,并设置 TTL;
  • 超时未支付则自动释放;
  • 定时任务兜底扫描。

七、整体防护链路总结

  1. 前端:按钮防连点、请求节流;
  2. 网关层:基础限流与防刷;
  3. 队列层:Redis Streams 削峰填谷;
  4. 库存层:Redis + Lua 保证原子扣减;
  5. 订单层:防重复下单(幂等性 + Token + 锁);
  6. 服务层:Sentinel 限流熔断;
  7. 善后机制:库存回滚 + 死信队列补偿。

结论

秒杀系统不是一招鲜的技术,而是一套 完整的多层防护方案

  • 前端负责“拦截垃圾流量”;
  • 队列与 Redis 负责“削峰与防超卖”;
  • 数据库与分布式锁负责“一致性保障”;
  • Sentinel 等限流机制提供“兜底保护”。

真正能撑住双 11 流量洪峰的,不是某个单一的优化,而是 从前端到数据库的全链路协同设计

当你构建出这样一套体系,面对千万级并发也能做到“系统稳如老狗”,再也不用担心用户抱怨“点了没反应,结果秒没了”。

AI大模型学习福利

作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。


因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值