牛券oneCoupon系统 第⑤章节:面试服务

eve2333 发布于 28 天前 53 次阅读


第31小节:压测牛券查询可用/不可用优惠券功能

压测注意事项

1. 什么是 QPS?

QPS 表示系统每秒能够处理的请求数量,可以理解为系统在一秒内处理了多少个查询(或请求)。常用于描述 接口API 请求 的处理能力,通常与 Web 服务、数据库查询等相关。

比如电商网站的商品查询,整个商品服务集群,对外提供的查询接口,每秒支撑 8800 次,那就意味着这个接口 QPS 是 8800。

2. 什么是 TPS?

TPS 表示系统每秒处理的事务数量。事务通常是一组操作的集合,需要保证整个操作集的原子性(全部成功或全部失败)。常用于描述涉及数据库操作或复杂业务场景的系统,如下单、支付等需要数据一致性的场景。在分布式系统中,TPS 是一个更严谨的指标,因为它强调 事务完整性和一致性,而不仅仅是处理请求的数量。

比如电商网站的下单接口,整个订单服务集群,对外提供的下单接口,每秒支撑 2800 次,那就意味着这个接口 TPS 是 2800。

3. 并发量指的 QPS 还是 TPS?

并发量基本等于吞吐量概念,大家知道即可。那聊到并发量或者吞吐量,指的是 QPS 还是 TPS 呢?

如果项目即有 QPS 场景同时也有 TPS 场景,我们一般是将 TPS 的,因为 TPS 的相关技术亮点比较多。拿牛券项目举例,QPS 有优惠券可用/不可用列表,TPS 有兑换优惠券场景。简而言之,如果面试官问并发量,那么一般泛指牛券兑换优惠券接口。

如果问具体 TPS 或者 QPS 的话,我们可以分别回答兑换优惠券和优惠券可用/不可用列表接口。

压测环境

软件架构中,系统设计是一部分,基础设施是一部分。系统设计一般就是看我们的代码和架构是如何运作的,比如代码中运行了先查询缓存 Redis 再查询数据库 MySQL,防止缓存击穿和穿透等设计。基础设施指的是部署的规格,比如 Redis 什么配置、MySQL 什么配置、部署了几台牛券 oneCoupon 服务以及每台部署机器的配置是多少。

在和面试官说时,一定要先明确自己的部署配置,比如:

  • 在自己本地电脑上进行的测试,电脑配置 MacBook M2 Max 12C64G。
  • 启动了一个牛券 SettlementApplication 结算服务。
  • 通过 Jmeter 配置了 80 个线程循环 1000 次压测,最终吞吐量 7033。

压测结果

Jmeter 我配置了 80 个线程,循环次数设置了 1000,意味着有 80 个线程会调用 1000 次牛券接口,最终会模拟产生 80000 次用户请求。

为什么设置 80 个线程?因为每台电脑的 CPU 性能不同,这个数据需要大家不断压测找到最优结果。其实再往上提升线程数还能提高点吞吐量,不过并不大,最终我就以 80 为结论了。

以我的电脑配置压测,结果如下所示:

image.png

Jmeter 压测脚本

将以下代码复制到文本编辑器中,保存为 oneCouponListQueryCoupons.jmx 文件,并使用 Jmeter 打开即可。

<?xml version="1.0" encoding="UTF-8"?><jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6"><hashTree><TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="牛券查询优惠券列表测试计划"><elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量"><collectionProp name="Arguments.arguments"/></elementProp></TestPlan><hashTree><ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组"><elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器"><stringProp name="LoopController.loops">1000</stringProp></elementProp><stringProp name="ThreadGroup.num_threads">80</stringProp><stringProp name="ThreadGroup.ramp_time">1</stringProp><boolProp name="ThreadGroup.scheduler">false</boolProp><stringProp name="ThreadGroup.duration"></stringProp><stringProp name="ThreadGroup.delay"></stringProp></ThreadGroup><hashTree><HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求"><boolProp name="HTTPSampler.postBodyRaw">true</boolProp><elementProp name="HTTPsampler.Arguments" elementType="Arguments"><collectionProp name="Arguments.arguments"><elementProp name="" elementType="HTTPArgument"><stringProp name="Argument.value">{&#xd;&quot;orderAmount&quot;: 226,&#xd;&quot;shopNumber&quot;: &quot;1810714735922956666&quot;,&#xd;&quot;goodsList&quot;: [&#xd;
    {&#xd;&quot;goodsNumber&quot;: &quot;001&quot;,&#xd;&quot;goodsAmount&quot;: 80&#xd;
    },&#xd;
    {&#xd;&quot;goodsNumber&quot;: &quot;002&quot;,&#xd;&quot;goodsAmount&quot;: 26&#xd;
    },&#xd;
    {&#xd;&quot;goodsNumber&quot;: &quot;003&quot;,&#xd;&quot;goodsAmount&quot;: 100&#xd;
    }&#xd;
  ]&#xd;
}</stringProp><stringProp name="Argument.metadata">=</stringProp></elementProp></collectionProp></elementProp><stringProp name="HTTPSampler.domain">127.0.0.1</stringProp><stringProp name="HTTPSampler.port">10030</stringProp><stringProp name="HTTPSampler.path">/api/settlement/coupon-query-sync</stringProp><stringProp name="HTTPSampler.method">POST</stringProp><boolProp name="HTTPSampler.follow_redirects">true</boolProp><boolProp name="HTTPSampler.use_keepalive">true</boolProp></HTTPSamplerProxy><hashTree/><ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树"><boolProp name="ResultCollector.error_logging">false</boolProp><objProp><name>saveConfig</name><value class="SampleSaveConfiguration"><time>true</time><latency>true</latency><timestamp>true</timestamp><success>true</success><label>true</label><code>true</code><message>true</message><threadName>true</threadName><dataType>true</dataType><encoding>false</encoding><assertions>true</assertions><subresults>true</subresults><responseData>false</responseData><samplerData>false</samplerData><xml>false</xml><fieldNames>true</fieldNames><responseHeaders>false</responseHeaders><requestHeaders>false</requestHeaders><responseDataOnError>false</responseDataOnError><saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage><assertionsResultsToSave>0</assertionsResultsToSave><bytes>true</bytes><sentBytes>true</sentBytes><url>true</url><threadCounts>true</threadCounts><idleTime>true</idleTime><connectTime>true</connectTime></value></objProp><stringProp name="filename"></stringProp></ResultCollector><hashTree/><ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告"><boolProp name="ResultCollector.error_logging">false</boolProp><objProp><name>saveConfig</name><value class="SampleSaveConfiguration"><time>true</time><latency>true</latency><timestamp>true</timestamp><success>true</success><label>true</label><code>true</code><message>true</message><threadName>true</threadName><dataType>true</dataType><encoding>false</encoding><assertions>true</assertions><subresults>true</subresults><responseData>false</responseData><samplerData>false</samplerData><xml>false</xml><fieldNames>true</fieldNames><responseHeaders>false</responseHeaders><requestHeaders>false</requestHeaders><responseDataOnError>false</responseDataOnError><saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage><assertionsResultsToSave>0</assertionsResultsToSave><bytes>true</bytes><sentBytes>true</sentBytes><url>true</url><threadCounts>true</threadCounts><idleTime>true</idleTime><connectTime>true</connectTime></value></objProp><stringProp name="filename"></stringProp></ResultCollector><hashTree/></hashTree><HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP信息头管理器"><collectionProp name="HeaderManager.headers"><elementProp name="" elementType="Header"><stringProp name="Header.name">Content-Type</stringProp><stringProp name="Header.value">application/json;charset=UTF-8</stringProp></elementProp></collectionProp></HeaderManager><hashTree/></hashTree></hashTree></jmeterTestPlan>

复制完成打开后,我们就可以开始测试了。执行线程和循环次数大家多进行调试,直到适配出最适合自己本地电脑的,这样面试时也能和面试官有理有据的输出。

截图如下所示:

image.png

我为用户领取了十几张优惠券,这样可以真实模拟出用户的行为习惯,避免只创建 1 个 2 个的优惠券,性能会好点,但是和真实场景有点出入。

Jmeter 请求压测单次结果如下所示:

{"code":"0","message":null,"data":{"availableCouponList":[{"id":"1843290380169883650","target":1,"goods":null,"type":2,"consumeRule":"{\"termsOfUse\":100,\"maximumDiscountAmount\":20,\"explanationOfUnmetConditions\":\"3\",\"discountRate\":\"0.6\",\"validityPeriod\":48}","couponAmount":20},{"id":"1843290379737870337","target":1,"goods":null,"type":1,"consumeRule":"{\"termsOfUse\":100,\"maximumDiscountAmount\":10,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":48}","couponAmount":10},{"id":"1843290372464947202","target":1,"goods":null,"type":0,"consumeRule":"{\"maximumDiscountAmount\":10,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":48}","couponAmount":10},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843870627484684290","target":1,"goods":"","type":0,"consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}","couponAmount":3},{"id":"1843290379234553857","target":0,"goods":"001","type":0,"consumeRule":"{\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":48}","couponAmount":3}],"notAvailableCouponList":[{"id":"1843290380593508353","target":1,"goods":null,"type":2,"consumeRule":"{\"termsOfUse\":300,\"maximumDiscountAmount\":40,\"explanationOfUnmetConditions\":\"3\",\"discountRate\":\"0.6\",\"validityPeriod\":48}","couponAmount":null}]},"requestId":null,"success":true,"fail":false}

第32小节:压测牛券并行查询可用/不可用优惠券功能

压测注意事项

什么是 QPS?什么是 TPS?项目并发量指的是 QPS 还是 TPS?详情查看上一章节。

压测环境

软件架构中,系统设计是一部分,基础设施是一部分。系统设计一般就是看我们的代码和架构是如何运作的,比如代码中运行了先查询缓存 Redis 再查询数据库 MySQL,防止缓存击穿和穿透等设计。基础设施指的是部署的规格,比如 Redis 什么配置、MySQL 什么配置、部署了几台牛券 oneCoupon 服务以及每台部署机器的配置是多少。

在和面试官说时,一定要先明确自己的部署配置,比如:

  • 在自己本地电脑上进行的测试,电脑配置 MacBook M2 Max 12C64G。
  • 启动了一个牛券 SettlementApplication 结算服务。
  • 通过 Jmeter 配置了 80 个线程循环 1000 次压测,最终吞吐量 5707。

压测结果

Jmeter 我配置了 80 个线程,循环次数设置了 1000,意味着有 80 个线程会调用 1000 次牛券接口,最终会模拟产生 80000 次用户请求。

为什么设置 80 个线程?因为每台电脑的 CPU 性能不同,这个数据需要大家不断压测找到最优结果。其实再往上提升线程数还能提高点吞吐量,不过并不大,最终我就以 80 为结论了。

以我的电脑配置压测,结果如下所示:

image.png

为什么并行用了线程池的接口,吞吐量反而不如同步查询的呢?在这里我梳理了几个原因:

  • 任务切换和线程池上下文切换开销:在多线程执行中,每个 CompletableFuture 都会被分配到一个线程去执行,而线程上下文切换和任务调度是有开销的。当任务非常细粒度(例如每个 forEach 中的操作)时,这种开销会占据较大的比例,从而抵消掉多线程带来的性能提升。
  • 线程资源竞争导致性能下降:当 ExecutorService 线程池中的线程数过多时,可能导致 CPU 过载,线程之间争抢 CPU 资源,频繁上下文切换反而使执行效率下降。
  • 共享资源访问时的锁竞争:在并行处理中,如果不同线程访问同一个共享资源(例如 availableCouponListnotAvailableCouponList)时,会存在锁竞争。即使使用了线程安全的集合(Collections.synchronizedList),在频繁的 add 操作中仍然会有同步锁的争抢,导致整体性能下降。
  • 非 I/O 密集型任务本身不适合多线程:处理逻辑(例如 handleCouponLogic)本质上是简单的对象转换和条件判断,并不是 CPU 密集型或者复杂的 I/O 处理。在这种情况下,多线程分解任务并不能显著提升效率,反而因线程切换带来更多开销,导致多线程版本不如单线程版本快。

那什么场景下适合用线程池并发编程?

  • I/O 密集型任务:I/O 密集型任务通常包含大量的等待时间,比如文件读写、网络请求、数据库操作等。由于 I/O 操作(例如磁盘读写、网络传输)在 CPU 上处于阻塞状态,因此此类任务非常适合使用多线程模型来提高吞吐量。多个线程可以在 I/O 等待时并行执行,从而充分利用 CPU 空闲时间。比如:数据库查询操作(如多数据库并行查询)、文件处理(文件上传、下载、大文件的读写)、网络调用(HTTP 请求、RPC 调用、Socket 通信)等。
  • CPU 密集型任务:CPU 密集型任务通常是一些需要大量计算的任务,比如复杂的数学运算、机器学习模型训练、图像处理等。对于这类任务,可以使用线程池将任务分发到不同的 CPU 核心执行,从而充分利用多核 CPU 的计算能力。比如:大量复杂计算(如排序、数据分析等)等。

Jmeter 压测脚本

将以下代码复制到文本编辑器中,保存为 oneCouponAsyncListQueryCoupons.jmx 文件,并使用 Jmeter 打开即可。

<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="牛券并行查询优惠券列表测试计划">
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组">
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器">
          <stringProp name="LoopController.loops">1000</stringProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">80</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求">
          <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
          <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="" elementType="HTTPArgument">
                <stringProp name="Argument.value">{&#xd;
  &quot;orderAmount&quot;: 226,&#xd;
  &quot;shopNumber&quot;: &quot;1810714735922956666&quot;,&#xd;
  &quot;goodsList&quot;: [&#xd;
    {&#xd;
      &quot;goodsNumber&quot;: &quot;001&quot;,&#xd;
      &quot;goodsAmount&quot;: 80&#xd;
    },&#xd;
    {&#xd;
      &quot;goodsNumber&quot;: &quot;002&quot;,&#xd;
      &quot;goodsAmount&quot;: 26&#xd;
    },&#xd;
    {&#xd;
      &quot;goodsNumber&quot;: &quot;003&quot;,&#xd;
      &quot;goodsAmount&quot;: 100&#xd;
    }&#xd;
  ]&#xd;
}</stringProp>
                <stringProp name="Argument.metadata">=</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          <stringProp name="HTTPSampler.domain">127.0.0.1</stringProp>
          <stringProp name="HTTPSampler.port">10030</stringProp>
          <stringProp name="HTTPSampler.path">/api/settlement/coupon-query</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
        </HTTPSamplerProxy>
        <hashTree/>
        <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
        <ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告">
          <boolProp name="ResultCollector.error_logging">false</boolProp>
          <objProp>
            <name>saveConfig</name>
            <value class="SampleSaveConfiguration">
              <time>true</time>
              <latency>true</latency>
              <timestamp>true</timestamp>
              <success>true</success>
              <label>true</label>
              <code>true</code>
              <message>true</message>
              <threadName>true</threadName>
              <dataType>true</dataType>
              <encoding>false</encoding>
              <assertions>true</assertions>
              <subresults>true</subresults>
              <responseData>false</responseData>
              <samplerData>false</samplerData>
              <xml>false</xml>
              <fieldNames>true</fieldNames>
              <responseHeaders>false</responseHeaders>
              <requestHeaders>false</requestHeaders>
              <responseDataOnError>false</responseDataOnError>
              <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
              <assertionsResultsToSave>0</assertionsResultsToSave>
              <bytes>true</bytes>
              <sentBytes>true</sentBytes>
              <url>true</url>
              <threadCounts>true</threadCounts>
              <idleTime>true</idleTime>
              <connectTime>true</connectTime>
            </value>
          </objProp>
          <stringProp name="filename"></stringProp>
        </ResultCollector>
        <hashTree/>
      </hashTree>
      <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP信息头管理器">
        <collectionProp name="HeaderManager.headers">
          <elementProp name="" elementType="Header">
            <stringProp name="Header.name">Content-Type</stringProp>
            <stringProp name="Header.value">application/json;charset=UTF-8</stringProp>
          </elementProp>
        </collectionProp>
      </HeaderManager>
      <hashTree/>
    </hashTree>
  </hashTree>
</jmeterTestPlan>
​

复制完成打开后,我们就可以开始测试了。执行线程和循环次数大家多进行调试,直到适配出最适合自己本地电脑的,这样面试时也能和面试官有理有据的输出。

我为用户领取了十几张优惠券,这样可以真实模拟出用户的行为习惯,避免只创建 1 个 2 个的优惠券,性能会好点,但是和真实场景有点出入。

Jmeter 请求压测单次结果如下所示:

{
    "code": "0",
    "message": null,
    "data": {
        "availableCouponList": [
            {
                "id": "1843290380169883650",
                "target": 1,
                "goods": null,
                "type": 2,
                "consumeRule": "{\"termsOfUse\":100,\"maximumDiscountAmount\":20,\"explanationOfUnmetConditions\":\"3\",\"discountRate\":\"0.6\",\"validityPeriod\":48}",
                "couponAmount": 20
            },
            {
                "id": "1843290379737870337",
                "target": 1,
                "goods": null,
                "type": 1,
                "consumeRule": "{\"termsOfUse\":100,\"maximumDiscountAmount\":10,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":48}",
                "couponAmount": 10
            },
            {
                "id": "1843290372464947202",
                "target": 1,
                "goods": null,
                "type": 0,
                "consumeRule": "{\"maximumDiscountAmount\":10,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":48}",
                "couponAmount": 10
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843870627484684290",
                "target": 1,
                "goods": "",
                "type": 0,
                "consumeRule": "{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}",
                "couponAmount": 3
            },
            {
                "id": "1843290379234553857",
                "target": 0,
                "goods": "001",
                "type": 0,
                "consumeRule": "{\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":48}",
                "couponAmount": 3
            }
        ],
        "notAvailableCouponList": [
            {
                "id": "1843290380593508353",
                "target": 1,
                "goods": null,
                "type": 2,
                "consumeRule": "{\"termsOfUse\":300,\"maximumDiscountAmount\":40,\"explanationOfUnmetConditions\":\"3\",\"discountRate\":\"0.6\",\"validityPeriod\":48}",
                "couponAmount": null
            }
        ]
    },
    "requestId": null,
    "success": true,
    "fail": false
}

第33小节:压测牛券优惠券秒杀券性能

压测注意事项

什么是 QPS?什么是 TPS?项目并发量指的是 QPS 还是 TPS?详情查看压测第一章节。

压测环境

软件架构中,系统设计是一部分,基础设施是一部分。系统设计一般就是看我们的代码和架构是如何运作的,比如代码中运行了先查询缓存 Redis 再查询数据库 MySQL,防止缓存击穿和穿透等设计。基础设施指的是部署的规格,比如 Redis 什么配置、MySQL 什么配置、部署了几台牛券 oneCoupon 服务以及每台部署机器的配置是多少。

在和面试官说时,一定要先明确自己的部署配置,比如:

  • 在自己本地电脑上进行的测试,电脑配置 MacBook M2 Max 12C64G。
  • 启动了一个牛券 EngineApplication 结算服务。
  • 通过 Jmeter 配置了 100 个线程循环 400 次压测,最终吞吐量 2588。

RocketMQ 我用自己电脑跑的 Docker,理论上如果代码和 RocketMQ 跑在同一台服务器或者同一个网段下应该会更快。

压测结果

秒杀有两个接口,一个是依赖数据库做实时扣减,这种稳定性较高,但是性能跟不上;一个是基于缓存和消息队列,极端情况下可能会有数据问题,但是从优惠券角度上可以容忍。所以,我们下面压测结果以后者展开。

Jmeter 我配置了 100 个线程,循环次数设置了 400,意味着有 100 个线程会调用 400 次牛券接口,最终会模拟产生 40000 次用户请求。

为什么设置 100 个线程?因为每台电脑的 CPU 性能不同,这个数据需要大家不断压测找到最优结果。其实再往上提升线程数还能提高点吞吐量,不过并不大,最终我就以 100 为结论了。

因为我们这个接口大部分是网络 IO,相比于前两次压测,大部分线程都在等待 IO,CPU 压力会小点。

以我的电脑配置压测,结果如下所示:

image.png

代码临时变更

1. 创建优惠券模板

我们在创建优惠券模板时,库存和单用户限领都设置高一些,避免因为优惠券没有库存或者被限领规则给校验异常。

{"name":"用户下单满10减3特大优惠","source":0,"target":1,"goods":"","type":0,"validStartTime":"2024-09-21 19:33:00","validEndTime":"2025-09-24 18:40:00","stock":20000000,"receiveRule":"{\"limitPerPerson\":20000000,\"usageInstructions\":\"3\"}","consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}"}

大家记得压完一次之后创建个新模版替换下这里,避免触发限领规则。如果触发限领规则,你会发现吞吐量怎么一次性提升这么多 哈哈。

image.png

2. 修改限领规则

将之前 stock_decrement_and_save_user_receive.lua 中的 14 改为 16,改为 16 后能支持的最大为 65535,这样我们单次压测的时候只要不高于 65535 的样本,都能够支持。

local function combineFields(firstField, secondField)-- 确定 SECOND_FIELD_BITS 为 16,因为 secondField 最大为 65535
    local SECOND_FIELD_BITS=16
​
    -- 根据 firstField 的实际值,计算其对应的二进制表示
    -- 由于 firstField 的范围是0-2,我们可以直接使用它的值
    local firstFieldValue = firstField
​
    -- 模拟位移操作,将 firstField 的值左移 SECOND_FIELD_BITS 位
    local shiftedFirstField = firstFieldValue *(2^SECOND_FIELD_BITS)
​
    -- 将 secondField 的值与位移后的 firstField 值相加
    return shiftedFirstField + secondField
end

Jmeter 压测脚本

将以下代码复制到文本编辑器中,保存为 oneCouponRedeemUserCouponByMQ.jmx 文件,并使用 Jmeter 打开即可。

<?xml version="1.0" encoding="UTF-8"?><jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6"><hashTree><TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="牛券兑换优惠券测试计划"><elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="用户定义的变量"><collectionProp name="Arguments.arguments"/></elementProp></TestPlan><hashTree><ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组"><elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器"><stringProp name="LoopController.loops">400</stringProp></elementProp><stringProp name="ThreadGroup.num_threads">100</stringProp><stringProp name="ThreadGroup.ramp_time">1</stringProp><boolProp name="ThreadGroup.scheduler">false</boolProp><stringProp name="ThreadGroup.duration"></stringProp><stringProp name="ThreadGroup.delay"></stringProp></ThreadGroup><hashTree><HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求"><boolProp name="HTTPSampler.postBodyRaw">true</boolProp><elementProp name="HTTPsampler.Arguments" elementType="Arguments"><collectionProp name="Arguments.arguments"><elementProp name="" elementType="HTTPArgument"><stringProp name="Argument.value">{&#xd;&quot;source&quot;: 0,&#xd;&quot;shopNumber&quot;: &quot;1810714735922956666&quot;,&#xd;&quot;couponTemplateId&quot;: &quot;1844952738815803394&quot;&#xd;
}</stringProp><stringProp name="Argument.metadata">=</stringProp></elementProp></collectionProp></elementProp><stringProp name="HTTPSampler.domain">127.0.0.1</stringProp><stringProp name="HTTPSampler.port">10020</stringProp><stringProp name="HTTPSampler.path">/api/engine/user-coupon/redeem-mq</stringProp><stringProp name="HTTPSampler.method">POST</stringProp><boolProp name="HTTPSampler.follow_redirects">true</boolProp><boolProp name="HTTPSampler.use_keepalive">true</boolProp></HTTPSamplerProxy><hashTree/><ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树"><boolProp name="ResultCollector.error_logging">false</boolProp><objProp><name>saveConfig</name><value class="SampleSaveConfiguration"><time>true</time><latency>true</latency><timestamp>true</timestamp><success>true</success><label>true</label><code>true</code><message>true</message><threadName>true</threadName><dataType>true</dataType><encoding>false</encoding><assertions>true</assertions><subresults>true</subresults><responseData>false</responseData><samplerData>false</samplerData><xml>false</xml><fieldNames>true</fieldNames><responseHeaders>false</responseHeaders><requestHeaders>false</requestHeaders><responseDataOnError>false</responseDataOnError><saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage><assertionsResultsToSave>0</assertionsResultsToSave><bytes>true</bytes><sentBytes>true</sentBytes><url>true</url><threadCounts>true</threadCounts><idleTime>true</idleTime><connectTime>true</connectTime></value></objProp><stringProp name="filename"></stringProp></ResultCollector><hashTree/><ResultCollector guiclass="StatVisualizer" testclass="ResultCollector" testname="聚合报告"><boolProp name="ResultCollector.error_logging">false</boolProp><objProp><name>saveConfig</name><value class="SampleSaveConfiguration"><time>true</time><latency>true</latency><timestamp>true</timestamp><success>true</success><label>true</label><code>true</code><message>true</message><threadName>true</threadName><dataType>true</dataType><encoding>false</encoding><assertions>true</assertions><subresults>true</subresults><responseData>false</responseData><samplerData>false</samplerData><xml>false</xml><fieldNames>true</fieldNames><responseHeaders>false</responseHeaders><requestHeaders>false</requestHeaders><responseDataOnError>false</responseDataOnError><saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage><assertionsResultsToSave>0</assertionsResultsToSave><bytes>true</bytes><sentBytes>true</sentBytes><url>true</url><threadCounts>true</threadCounts><idleTime>true</idleTime><connectTime>true</connectTime></value></objProp><stringProp name="filename"></stringProp></ResultCollector><hashTree/></hashTree><HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP信息头管理器"><collectionProp name="HeaderManager.headers"><elementProp name="" elementType="Header"><stringProp name="Header.name">Content-Type</stringProp><stringProp name="Header.value">application/json;charset=UTF-8</stringProp></elementProp></collectionProp></HeaderManager><hashTree/></hashTree></hashTree></jmeterTestPlan>

复制完成打开后,我们就可以开始测试了。执行线程和循环次数大家多进行调试,直到适配出最适合自己本地电脑的,这样面试时也能和面试官有理有据的输出。

Jmeter 请求压测单次结果如下所示:

{"code":"0","message":null,"data":null,"requestId":null,"fail":false,"success":true}

第34小节:压测优惠券推送任务分发性能

测试注意事项

因为咱们优惠券分发功能不存在很大的并发行为,所以我们的测试路径是在指定数量下的分发性能,比如为 100 万用户分发优惠券需要多久?

前情回顾一下,在 v1 版本的基础上,通过引入批处理和保存失败记录的逻辑,大幅提升了业务执行性能,并支持查看 Excel 分发的错误记录。相同的 5000 条记录,v1 版本执行时间约为 1 分钟,而 v2 版本仅需 1 秒,实现了接近 60 倍的性能提升。

如果正常来说执行 100 万条记录是除以 5000 等于 200 秒,具体中可能会有相关变化,我们一起拭目以待。

压测环境

软件架构中,系统设计是一部分,基础设施是一部分。系统设计一般就是看我们的代码和架构是如何运作的,比如代码中运行了先查询缓存 Redis 再查询数据库 MySQL,防止缓存击穿和穿透等设计。基础设施指的是部署的规格,比如 Redis 什么配置、MySQL 什么配置、部署了几台牛券 oneCoupon 服务以及每台部署机器的配置是多少。

在和面试官说时,一定要先明确自己的部署配置,比如:

  • 在自己本地电脑上进行的测试,电脑配置 MacBook M2 Max 12C64G。
  • 启动牛券 DistributionApplication 和 MerchantAdminApplication 服务。

RocketMQ 我用自己电脑跑的 Docker,理论上如果代码和 RocketMQ 跑在同一台服务器或者同一个网段下应该会更快。

1. 创建优惠券模板

首先呢,咱们先创建好优惠券模板,在后管服务里调用创建接口,获取到对应的优惠券模板 ID 准备调用创建分发任务。

请求数据如下所示:

{"name":"用户下单满10减3特大优惠","source":0,"target":1,"goods":"","type":0,"validStartTime":"2024-09-21 19:33:00","validEndTime":"2025-09-24 18:40:00","stock":20000000,"receiveRule":"{\"limitPerPerson\":20000000,\"usageInstructions\":\"3\"}","consumeRule":"{\"termsOfUse\":10,\"maximumDiscountAmount\":3,\"explanationOfUnmetConditions\":\"3\",\"validityPeriod\":\"48\"}"}

2. 生成优惠券分发 Excel

还记得我们的 ExcelGenerateTests 单元测试类么,把 writeNum 改为 100 万,调用 testExcelGenerate 生成即可。然后查看项目根目录下的 tmp 文件夹下的 oneCoupon任务推送Excel.xlsx 文件,复制绝对路径在下面步骤请求。

publicfinalclassExcelGenerateTests{
​
    /**
     * 写入优惠券推送示例 Excel 的数据,自行控制即可
     */privatefinalint writeNum =1000000;privatefinalFaker faker =newFaker(Locale.CHINA);privatefinalString excelPath =Paths.get("").toAbsolutePath().getParent()+"/tmp";
​
    @TestpublicvoidtestExcelGenerate(){if(!FileUtil.exist(excelPath)){FileUtil.mkdir(excelPath);}String fileName = excelPath +"/oneCoupon任务推送Excel.xlsx";EasyExcel.write(fileName,ExcelGenerateDemoData.class).sheet("优惠券推送列表").doWrite(data());}
​
    // ......}

3. 创建优惠券分发任务

还是在后管系统中,调用 商家创建优惠券推送任务 接口,并将下述的 fileAddress 替换为大家生成的 Excel 文件地址即可。

{"taskName":"发送百万优惠券推送任务","fileAddress":"xxx","couponTemplateId":1845032811019386882,"sendType":0}

这样就可以触发百万分发优惠券任务的流程了。

压测结果

根据我这边的实时优惠券分发测试,结果如下所示:

image.png

有两个关键时间:

  • 创建时间:2024-10-12 17:26:40
  • 完成时间:2024-10-12 17:30:10

两者相差 210 秒,也就是意味着 3 分钟半我们完成了百分优惠券的分发流程,如果是单线程的逻辑,可能就得慢到猴年马月去了。

这个数据基本上符合我们的预期,大家可以本地跑一下,看执行时间差别大不大。