实现秒杀一人一单并发压测 _ 黑马点评业务实战
SpringBoot 实现秒杀一人一单并发压测 | 黑马点评业务实战
一、前言
在电商秒杀、优惠券抢购业务中,并发安全 和 接口承载能力 是核心技术痛点。黑马点评项目中的优惠券秒杀模块,核心需求是 一人一单(单个用户仅能抢购一张优惠券),在高并发场景下极易出现超卖、重复下单、并发冲突等问题。
为了验证秒杀接口的并发安全性、校验一人一单拦截逻辑的有效性,同时测试接口 QPS 承载能力,我们需要编写专业的并发压测代码。本文将详细拆解一套基于 Junit+线程池的秒杀并发压测实战代码,适配黑马点评完整业务场景。
二、压测场景说明
1. 业务规则
-
优惠券秒杀限制:一人一单,同一个用户只能成功抢购一次
-
基础能力:控制优惠券库存,防止超卖
-
并发场景:多用户、高频次重复请求秒杀接口,模拟真实高并发抢购场景
2. 压测目标
-
验证 一人一单 拦截逻辑是否生效,用户重复抢购是否被正确拦截
-
验证秒杀接口并发安全,是否存在超卖问题
-
统计接口 QPS、总耗时、成功/失败请求数,评估接口性能
三、完整压测代码
基于 SpringBootTest、Junit、线程池实现并发压测,代码贴合黑马点评原生业务,无需额外改造即可直接运行:
@SpringBootTest(classes = HmDianPingApplication.class)
class SeckillPressureTest {
@Autowired
private IVoucherOrderService voucherOrderService;
// 初始化固定线程池,模拟并发请求
private final ExecutorService es = Executors.newFixedThreadPool(500);
@Test
void testSeckillQps() throws InterruptedException {
// 压测核心参数:500 并发线程,每个线程请求 20 次
int threadCount = 500;
int perThread = 20;
// 待秒杀的优惠券 ID,替换为业务真实有效 ID
long voucherId = 24;
// 线程计数器,等待所有并发线程执行完毕
CountDownLatch latch = new CountDownLatch(threadCount);
// 存储成功的订单 ID,去重统计有效下单数(适配一人一单)
ConcurrentHashMap<Long, Boolean> successMap = new ConcurrentHashMap<>();
// 统计失败请求总数
AtomicInteger failCount = new AtomicInteger(0);
// 记录压测开始时间
long begin = System.currentTimeMillis();
// 批量创建并发线程
for (int i = 0; i < threadCount; i++) {
final long userId = i + 1;
es.submit(() -> {
try {
// 模拟登录用户,绑定当前线程用户 ID
UserDTO userDTO = new UserDTO();
userDTO.setId(userId);
UserHolder.saveUser(userDTO);
// 单线程循环多次发起秒杀请求,模拟高频重试
for (int j = 0; j < perThread; j++) {
Result result = voucherOrderService.seckillVoucher(voucherId);
if (result.getData() != null) {
// 下单成功,存入订单 ID 自动去重
successMap.put((Long) result.getData(), true);
} else {
// 下单失败,计数器累加
failCount.incrementAndGet();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 单个线程执行完毕,计数器减一
UserHolder.removeUser();
latch.countDown();
}
});
}
// 阻塞等待所有线程执行完成
latch.await();
// 记录压测结束时间
long end = System.currentTimeMillis();
// 计算压测核心指标
int totalRequest = threadCount * perThread;
long costTime = end - begin;
double qps = (double) totalRequest / (costTime / 1000.0);
// 打印压测结果
System.out.println("================== 秒杀压测结果 =================");
System.out.println("总请求数:" + totalRequest);
System.out.println("成功下单数量:" + successMap.size());
System.out.println("接口QPS:" + String.format("%.2f", qps));
System.out.println("总耗时:" + costTime + "ms");
}
}
四、核心代码逻辑详解
1. 并发场景模拟
代码通过 Executors.newFixedThreadPool(500) 创建 500 个固定线程,模拟高并发抢购场景。同时设置 每个线程循环请求 20 次,模拟用户频繁重试抢购的真实场景,最大程度还原线上流量峰值。
通过 CountDownLatch 实现线程阻塞,保证主线程等待所有并发子线程执行完毕后,再统计压测数据,避免数据统计不全的问题。
2. 一人一单场景适配
压测中为每个线程分配 独立唯一的用户 ID(userId = i + 1),模拟 500 个不同用户同时抢购。同时使用 ConcurrentHashMap 存储成功的订单 ID,利用 Map 键唯一的特性 自动去重。
核心校验逻辑:单个用户无论请求多少次,最终仅能生成一笔有效订单。如果压测成功数大于用户数,说明接口存在一人多单的并发漏洞。
3. 压测指标统计
代码自动计算三大核心性能指标,无需手动统计:
-
总请求数:并发线程数 × 单线程请求次数,本次压测总请求 10000 次
-
有效成功数:去重后的真实下单订单数,校验一人一单和库存防超卖逻辑
-
QPS:每秒处理的请求数,衡量接口并发处理能力
-
总耗时:整体压测执行时长,辅助评估接口响应速度
4. 线程用户隔离
通过 UserHolder.saveUser(userDTO) 模拟用户登录状态,黑马点评项目通过 ThreadLocal 存储用户信息,每个线程绑定独立用户 ID,完美模拟多用户并发场景,避免用户上下文混乱导致的压测数据失真。
五、压测结果分析与校验标准
1. 正常结果标准(接口无漏洞)
-
有效成功下单数 ≤ 优惠券库存数:无超卖问题
-
有效成功下单数 ≤ 并发用户数(500):一人一单拦截逻辑生效,无用户重复下单
-
失败请求数为大量正常拦截请求,属于业务正常现象
2. 异常结果排查
-
成功数大于库存数:秒杀接口无库存锁,存在超卖 Bug,需优化分布式锁、乐观锁逻辑
-
成功数大于用户数:一人一单拦截失效,未做用户下单唯一校验,并发下出现重复下单
六、压测优化拓展建议
1. 现有代码优势
零依赖、轻量级,无需 JMeter、LoadRunner 等第三方压测工具,基于原生 Java 代码即可完成并发压测,适配项目单元测试流程,可集成到项目测试体系中。
2. 生产级优化方向
-
线程池优化:替换为 ThreadPoolExecutor 自定义线程池,规避 Executors 线程池缺陷
-
参数可配置化:将线程数、请求次数、优惠券 ID 写入配置文件,无需改代码即可调整压测参数
-
结果日志持久化:将压测结果写入日志文件,方便后续性能对比分析
-
超时机制:增加接口请求超时判断,统计超时失败率
七、总结
本文实现的秒杀并发压测代码,精准适配黑马点评 一人一单 核心业务场景,不仅能够完成接口 QPS 性能压测,更核心的是可以 校验并发安全逻辑,快速定位超卖、重复下单等线上高危问题。
在秒杀功能开发完成后,通过该压测用例可快速验证代码健壮性,是电商秒杀业务开发、测试、性能调优的必备实战方案。轻量级代码、简单易落地,适合初学者学习并发压测思想,也可直接用于项目实战。
(注:部分内容可能由 AI 生成)

浙公网安备 33010602011771号