Spring Boot抽奖系统全流程测试:从功能、并发到监控与安全

发布时间:2026/9/9 4:58:19
Spring Boot抽奖系统全流程测试:从功能、并发到监控与安全 直接说结论我把一个基于Spring Boot的抽奖系统从功能、并发、监控到安全全流程测了一遍最终沉淀出一份可以直接用于项目验收和汇报的测试报告。抽奖系统这个业务看起来不复杂用户点击抽奖、服务端返回奖品、后台处理发奖但真正进入测试阶段会发现资格校验、库存扣减、异步发奖、防刷限流、数据一致性全都堆在一个看似简单的接口链路里。这篇帖子就围绕这次测试全过程来做拆解重点讲清测试用例怎么设计、JMeter压测参数怎么定、Redis Stream消费和库存并发怎么验证、Actuator监控指标怎么用于定位问题以及最终测试报告应该怎么整理。适合正在负责抽奖、秒杀、活动类系统的开发、测试同学参考也适合准备写Spring Boot项目测试报告但不知道从哪下手的人直接照着裁剪。1. 抽奖系统的测试范围和实施路径1.1 核心业务链路拆解与风险点分析我这次负责的系统整体技术栈是Spring Boot 2.7 JDK17 MySQL 8 Redis 6抽奖模块本身不复杂但从用户点击到奖品到账要经过这么一条链路用户登录并获取JWT令牌 → 后端校验用户身份与活动资格 → 检查该用户当日剩余抽奖次数 → 扣减抽奖机会 → 根据权重算法计算抽奖结果 → 同步扣减奖品库存或走Redis预扣库存 → 返回中奖信息 → 异步发放奖品 → 记录抽奖流水并触发通知。这个链路里我拆出了四个高风险节点资格校验是否幂等会不会出现“点击一次但扣了多次次数”或者“没有资格但也能抽中”奖品库存是否会被超发尤其是在高并发窗口期内抽奖结果计算与库存扣减是否原子如果两边不是同一事务很容易出现结果和奖品对不上发奖动作和抽奖动作分离后发奖消息丢了或者重复消费会不会导致奖品不发放或重复发放。测试报告里如果不能把这些节点的风险说明白只贴一堆“用例通过”的结论那这份报告基本没什么参考价值。所以我后续所有测试执行都是围绕这些风险点逐个做定向验证的。1.2 测试范围划分与方案选择整个测试范围我划分成模块来看用户鉴权模块、抽奖活动模块、奖品库存模块、异步发奖模块、后台奖品管理模块。覆盖范围太大时如果一把抓会很乱建议按“核心链路优先、后台功能其次”的方式排优先级。测试策略上我分四层来做第一层是功能测试用JUnit 5 MockMvc跑接口自动化用例再补少量手工场景第二层是并发测试用JMeter对抽奖接口做阶梯加压验证系统稳定性和资源瓶颈第三层是可靠性测试专门针对Redis Stream消息、库存扣减这类数据一致性场景做验证第四层是平台监控与安全验证引入Micrometer Spring Boot Actuator采集JVM、线程池、连接池、接口RT等指标同时从接口层检查越权和防刷漏洞。2. 功能测试用例设计从鉴权、抽奖到发奖闭环2.1 必测的7类边界场景用例清单抽奖接口功能测试看起来是“调一个接口看返回”但用例设计其实比想象中细很多。我整理了一份必测清单适合多数抽奖类系统直接用未携带token请求抽奖接口系统必须拦截并返回401而不是返回业务数据或直接500token有效但活动处于未开始/已结束状态时要返回对应业务错误码用户抽奖次数为0时抽奖接口不能继续抽且错误信息要能引导用户参与其他活动获取次数奖品库存为0时要能触发兜底奖品逻辑或者提示奖品已抽完不能抛异常同一用户连续点击抽奖按钮服务端要能通过幂等令牌去重不能并发扣减两次次数奖品权重列表总和要等于100否则会出现概率异常我测试时特意构造了一组总和不为100的数据确认系统能拦截这类配置错误奖品设置了活动有效期不在有效期内的奖品不能被抽中同时后台配置变更后需要缓存刷新。这7类用例覆盖了接口层最常见的bug来源。尤其是用户身份校验如果系统只用请求参数里的uid去识别用户身份那就是一个严重的越权漏洞我在安全测试部分还会细讲。2.2 MockMvc自动化测试的断言写法与注意事项功能用例我用Spring Boot的MockMvc写成了自动化用例。这里给出一段实际可跑的示例方便你直接参考。SpringBootTest AutoConfigureMockMvc class LotteryDrawApiTest { Autowired private MockMvc mockMvc; Test DisplayName(带有效token抽奖返回成功且奖品名不为空) void drawWithValidTokenShouldReturnPrize() throws Exception { String token loginAndGetToken(tester001); mockMvc.perform(post(/api/lottery/draw) .header(Authorization, Bearer token) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath($.code).value(0)) .andExpect(jsonPath($.data.prizeName).isNotEmpty()); } Test DisplayName(无token请求抽奖接口应返回401) void drawWithoutTokenShouldReturnUnauthorized() throws Exception { mockMvc.perform(post(/api/lottery/draw)) .andExpect(status().isUnauthorized()); } }这里有几个经验提醒。第一如果测试类加了Transactional抽奖后的Redis数据不会被回滚因为Redis本身不支持Spring事务回滚。抽奖次数扣减或库存扣减这类涉及Redis的用例要么在测试结束后手动清理key要么用独立的测试Redis实例避免脏数据影响其他用例。第二写断言时不要只盯着HTTP状态码和业务code还要关注关键字段。比如抽奖成功的返回值里建议断言流水号不为空、中奖结果带唯一requestId。因为后续对账、异步发奖都依赖这个字段。第三数据库层面造数时直接用JdbcTemplate或Repository在测试前置方法里插入数据比通过接口一层层造数据快得多而且可控性更强。比如构造库存剩余0的情况直接update prize_stock set stock 0 where id ?即可。2.3 用户越权与防刷相关的典型用例越是看起来简单的抽奖系统越容易在权限控制上翻车。我这次功能测试阶段就发现了一个很典型的越权问题后台接口/api/admin/prize/update只校验了用户是否登录没有校验用户角色。一个普通用户登录后只要能猜到后台接口路径就能修改奖品概率。这个问题的修复方案不复杂加一个基于角色的拦截器或使用Spring Security方法级注解PreAuthorize(hasRole(ADMIN))即可。但问题是很多团队在联调阶段只测了“管理员能不能用”根本没测“普通用户能不能用”。防刷相关的用例也不能漏。抽奖系统最容易被打的点就是无限刷抽奖机会。我测试时特地检查了服务端的次数扣减逻辑发现有一个版本把“剩余次数”直接放在前端传的参数里服务端没有从缓存或数据库读取这等于用户可以随意传一个很大的值进来。正确的做法是资格判断必须服务端统一管理比如将用户抽奖次数放在Redis每次抽奖用Lua脚本原子扣减。3. JMeter压测全程场景、参数与监控3.1 单接口压测场景的搭建与关键参数选取性能测试工具我用的JMeter环境是测试服务器4C8G、MySQL和Redis分别部署在独立的2C4G容器上。压测目标主要聚焦三个接口每日签到获取抽奖次数、抽奖接口、后台查询抽奖记录接口。其中抽奖接口是核心压测压力集中在这个接口上。我在JMeter里建了一个线程组模型压测场景是这样的首先用CSV数据文件存放测试用户token每个线程启动时读取一个token抽奖请求使用HTTP Header管理器统一携带Authorization请求头每次迭代之间加一个固定定时器模拟用户思考延迟防止性能数据被无谓空转给稀释掉线程数先做基准测试1个线程跑3分钟观察单用户下接口响应时间作为后续对比的基线。然后阶梯加压100并发、200并发、500并发各跑5分钟。这个时间长度比较合适太短的话JIT尚未充分预热、线程池还没打满数据会失真。压测线程数量确定后需要关注几个关键参数ramp-up period我一般设置成1秒让线程快速建立起来模拟活动刚开始的瞬时冲击。如果设置成60秒慢慢爬坡压不出真正的峰刺效果。3.2 用JMeter生成可视化HTML测试报告很多测试人员跑完JMeter只会看聚合报告里的平均值其实JMeter能做到更直观的可视化报告而且不需要装第三方插件。在命令行下执行压测脚本直接指定结果文件以及要生成报告的目录jmeter -n -t lottery_draw.jmx -l lottery_result.jtl -e -o ./lottery_report参数里的-n表示以非GUI模式运行-t指定测试计划路径-l保存原始结果日志-e表示生成HTML报告-o是报告输出目录。跑完后用浏览器直接打开./lottery_report/index.html就能看到整份报告里面包含吞吐量趋势、响应时间百分位、活跃线程数随时间的变化、错误率统计等图表。这里有几个踩坑提醒每次压测前一定要把lottery_result.jtl删掉或者换新的文件名这个文件是追加追加模式不清空会把上一轮数据混进来报告目录也要是一次性的JMeter不会覆盖已有目录目录存在会直接报错关键接口的压测报告中最值得看的不是平均响应时间而是90th pct和99th pct分别代表90%和99%请求的耗时上限这两个值比平均值更能反映系统在长尾请求上的表现。对于抽奖系统我的经验是平均RT在200毫秒内、99th pct不超过1秒、错误率低于0.1%才能算勉强能扛得住日常活动流量。3.3 压测中发现并解决的三个性能瓶颈压测过程中我一共定位到三个比较典型的性能瓶颈这里挑出来说一下都是Spring Boot抽奖类系统大概率会遇到的问题。第一个是数据库连接池被打满。系统用的是HikariCP连接池默认maximum-pool-size只有10。当并发从200升到500时日志里频繁出现HikariPool-1 - Connection is not available, request timed out。这个调整不能盲目加到很大我先看了压测期间数据库服务端最大连接数再把HikariCP的maximum-pool-size调整到100minimum-idle保持默认同时把max-lifetime调到和MySQL的wait_timeout接近避免连接被数据库服务端断开后还滞留在池中。第二个是抽奖资格扣减逻辑用了一个重量级Synchronized方法块把整个抽奖流程锁住。这个方案在单个实例下能保证线程安全但在压测下吞吐量被锁得惨不忍睹500并发时TPS只有不到80。我给团队的建议是把资格扣减和库存预扣改造成Redis Lua脚本原子执行释放业务线程的竞争压力。改造后单机TPS有了数量级的提升。第三个是Redis连接不稳定。代码里有一个地方直接把Redis连接工厂每次调用时新建了一个连接结果在500并发下大量抛RedisConnectionFailureException。这是明显的资源管理问题修复方式很简单注入统一的RedisTemplate单例。同时检查Redis客户端连接池确保max-total和max-idle配置与压测并发量匹配。4. 高并发下抽奖一致性与消息可靠性的专项验证4.1 Redis Stream用于异步发奖的测试要点这个项目里抽奖动作和发奖动作是异步分离的抽奖接口只负责确认中奖结果然后把发奖消息丢给队列由独立消费者处理发奖。项目选择的是Redis Stream作为消息队列相比直接使用Redis List它能支持消费组、消息确认和消息持久化。系统会以发奖消息的消费情况为准做后续补偿。我在测试报告里专门写了这块的验证过程。核心验证点有三个消费组是否能保证同一条消息只被一个消费者处理消费者异常崩溃后已读取未确认的消息是否还能被其他消费者拉取到常规消息积压时新启动的消费者应该从哪里开始读取。关于Spring Boot Redis Stream如何拉取队列消息项目里用的是StreamMessageListenerContainer大致结构是定义监听容器并注册消息监听器消费者需要手动确认ack。如果这里直接使用自动确认一旦消费者处理消息时应用宕机消息就丢失了。Redis Stream的比较稳妥方式是先处理业务逻辑再调用acknowledge方法确认消息已经消费完成。我记得有一次压测模拟了消费者实例宕机重启后发现有一批消息始终没有被处理。原因就是这条消息因为处理异常一直处于pending状态但没有被转入死信队列。后来我加了一个定时任务扫描XINFO GROUPS里的pending消息把超过5分钟仍未确认的消息捞出来重新投递到死信队列由补偿Job重新消费。这也是做发奖类异步任务必须有的兜底方案。4.2 库存防超发的并发压测与验证方法库存扣减是最能验证抽奖系统高并发一致性的场景。我开始测试时发现系统原来的实现是先查库存是否充足再执行stock - 1中间隔着业务逻辑和网络IO在并发40左右库存就超发了等压测完一看商品库存剩余变成了负数。这个问题的根源就是所谓的“先查后扣”模式在多线程并发下无法保证原子性。修复后的逻辑我印象很深直接用带条件的更新语句把库存扣减和判断合并成一个原子操作UPDATE prize_stock SET stock stock - 1 WHERE prize_id ? AND stock 0;如果返回的影响行数等于0说明库存不足此时需要回滚之前扣减的抽奖次数。在高并发场景下为了避免频繁落库更新造成行锁竞争还可以先用Redis预扣库存再把扣减明细异步落库。我当时从压测角度提供了一个参考公式库存扣减并发从下单开始到达数据库链路中间每多一个非原子操作超发的概率就指数级增加。测试验证方法比较直接把某个奖品库存设置为200然后用500个用户并发抽奖每个用户只允许抽一次、系统中奖规则固定为该奖品最终统计成功中奖的用户数量必须严格等于200。我跑完后还会去数据库确认流水表里面中奖的prizeId一致同时核对库存扣减记录数任何一步对不上都算测试失败。4.3 消费失败、消息滞留时的补偿逻辑测试异步发奖逻辑里不能只测阳光路径。我把消费者处理消息的过程中主动抛了异常模拟奖品服务短暂不可用、数据库超时、外部接口调用失败三种场景。验证点包括异常消息是否进入重试队列、重试次数达到上限后是否进入死信主题、补偿任务扫描的间隔配置是否合理。这个环节我测试时发现一个问题补偿任务默认每5分钟跑一次但如果Redis Stream积压的消息太多消费者拉取消息会有一定延迟导致部分实物奖品发放时间远远晚于用户抽中时间引发用户投诉。解决方向是给实物类奖品单独建一个高优先级消费组并把消费线程数调大同时缩小定时任务对账时间窗口从5分钟调整到1分钟。做这类测试调试时建议在Redis客户端里使用XRANGE这类命令手动查看消息内容和消费状态可以非常直观地看到消息是不是卡在某个消费者上。5. Actuator监控与接口安全测试的落地记录5.1 引入Micrometer和Actuator后如何看监控指标压测过程中如果没有实时监控性能瓶颈往往只能靠猜。项目里引入了Spring Boot Actuator和Micrometer把应用指标以Prometheus格式暴露出来。依赖配置如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency然后在application.yml里暴露需要的端点management: endpoints: web: exposure: include: health,info,prometheus,metrics endpoint: health: show-details: always压测时我除了盯JMeter报告还会定期访问/actuator/prometheus重点看这几个指标JVM堆内存使用、活跃线程数、HikariCP连接池活跃连接数、接口请求耗时直方图。压测到300并发时通过指标曲线发现GC非常频繁老年代内存一直在涨这才定位到系统里有一个集合对象一直被静态变量持有属于典型的内存泄漏隐患。需要特别留意的是如果是在生产环境接入Actuator必须给/actuator端点加访问鉴权。最直接的方法是Spring Security拦截或者通过网关统一只放通health等基础端点。否则项目运行信息对任何人可见属于严重的信息暴露风险。5.2 抽奖系统安全漏测的典型问题与修正安全测试不是非得拿扫描器扫一遍才是完整的。我在接口层面检查时发现了几个手测就能查出来的问题这类问题很典型。第一个问题是抽奖接口依赖前端传参判断当前用户对应的活动昵称或用户ID而后端从JWT中解析出的用户ID被忽略导致用户可以拿着自己的token去操作其他用户的数据。修复方案是从任意请求中解析用户身份都只信任安全上下文里的数据。第二个问题是后台奖品管理模块没有任何角色校验只要是登录用户就能调用修改奖品概率的接口。这种问题在联调阶段不太容易被发现因为测试同学通常只关注抽奖主流程。我的建议是在项目的全局安全配置里统一加上接口路径与角色匹配规则而不是在每个Controller方法里零散加判断。第三个问题是抽奖接口没有限流措施。我用JMeter模拟同IP高频请求时发现用户可以通过脚本在极短时间内发起几百次抽奖请求如果服务端没有幂等校验抽奖次数会被迅速扣光同时给系统造成压力。常规方案是接入Redis Lua限流逻辑对同一用户或同一IP做滑动窗口限流同时针对抽奖这类关键写入接口做每秒最大请求数限制。5.3 SQL注入与参数非法性检查我在测试参数层面时还顺手检查了常见SQL注入风险。比如很多列表查询接口喜欢用StringBuilder拼接查询条件如果奖品名称这类字段没有使用预编译参数恶意传入 or 11就有可能导致全表数据被拉取。测试发现项目在抽奖记录导出接口存在类似问题后来统一替换成MyBatis的#{}参数占位符解决。参数非法性也不能忽略。抽奖活动的活动ID、奖品ID这类数值参数后端必须做范围校验。如果前端传入一个不存在的奖品ID后端返回了“非法礼品”的提示而不是直接抛出NumberFormatException才是合理的异常处理。6. 测试报告的整理思路与问题速查6.1 测试报告该写什么框架、指标和数据来源网上流传的测试报告模板形形色色但最终能通过项目验收和领导评审的还是那些信息密度高、数据来源明确、结论可直接执行的内容。我这份报告核心按下面几块组织测试概述写明测试对象、版本、时间、测试环境与测试数据来源需求覆盖矩阵每个模块对应哪些需求点、哪些用例覆盖、是否全部通过用例执行统计共执行多少条用例、通过数、失败数、阻塞数、跳过数及原因缺陷统计与分析缺陷数量、严重程度分布、模块分布、发现趋势与遗留问题性能测试结论核心接口在不同并发下的TPS、RT百分位、错误率、资源使用情况风险评估与上线建议客观写清楚哪些风险已经修复哪些风险暂时接受哪些问题必须上线前解决。其中性能和缺陷统计最好都用图表呈现不要写一堆晦涩数据。比如缺陷统计我建议做成表格缺陷级别、对应模块、当前状态、修改建议一目了然。下面是我我这次项目测试报告中的一个小节示意你可以看下这种格式严重级别问题描述所属模块状态建议处理方式P0并发下奖品库存超卖奖品库存模块已修复改为原子扣减补充并发场景回归用例P1普通用户可调用后台修改奖品接口用户权限模块已修复增加角色权限拦截器P2异步发奖无死信队列补偿异步发奖模块已修复增加消费失败重试与死信扫描6.2 压测与功能测试通用问题速查表测试过程里遇到的问题如果能沉淀成一张速查表后续排障效率会高很多。我这边的通用问题速查表长这样可以直接当参考问题现象可能原因排查方向与处理建议高并发时接口报“Connection is not available”HikariCP连接池配置过小查看活跃连接数曲线调大maximum-pool-size抽奖成功率低且库存快速为负先查库存再扣库存未做原子操作使用带条件UPDATE或Redis Lua脚本扣减同一条发奖消息被多次处理消费者未做幂等或没有消息确认在消息处理逻辑增加流水号唯一约束或手动ack压测TPS上不去CPU负载很低业务代码存在串行锁或同步IO等待抓线程栈定位BLOCKED状态线程Redis Stream消息积压但消费者无动作消费线程挂起或pollTimeout过长检查消费者容器线程数与pollTimeout配置接口偶发500且日志出现OOM内存泄漏或堆内存配置过小通过Actuator观察GC和堆内存指标做堆dump分析6.3 几个容易被忽略的细节最后讲几个容易被忽略但很实用的细节。测试抽奖次数扣减时一定要验证“扣次数成功但抽奖事务回滚”的场景。比如用户发起抽奖先扣了次数后面执行奖品库存扣减时失败系统能不能把次数补偿回去。如果没做这一步用户白白损失一次抽奖机会线上投诉率会很高。测试异步发奖时记得关掉消费者进程模拟服务宕机观察消息在Redis Stream里的pending状态。这一步最容易暴露消息丢失问题比单纯看几行日志靠谱得多。测试报告里的性能结论要带上压测期间的环境参数。同一个接口在4C8G和8C16G机器上的表现完全不同不注明环境的话半个月后别人拿到报告根本没法复用。还要提醒一点不要把功能测试数据和性能测试数据混在同一张表里。功能测试可能因为环境不稳定出现偶发超时如果这些数据混到性能测试的统计口径中会污染最终的TPS和RT结论。我在报告里会把两者拆开功能测试的执行记录单独生成一份附件存档。

相关新闻