You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免JMeter多线程执行JSR223 Sampler生成重复测试数据

问题

在JMeter中搭建测试场景,通过JSR223 Sampler使用Java StringBuilder类动态生成测试数据并传入SOAP请求体。根据业务逻辑,部分测试数据行可重复,但有一行需随每个请求保持唯一。

此前用JMeter计数器组件实现该行唯一性效果良好,但需实现更复杂逻辑:生成唯一行的计数器需关联BatchID单独计数(而非随请求顺序递增),因此将计数器逻辑嵌入脚本。脚本运行正常,但出现不同线程执行JSR223 Sampler生成重复请求的问题,重复率约1%,需彻底消除。

观察到的现象:

  • 50用户、1秒启动时间时,测试初期错误率更高;
  • 启动时间改为200秒时重复率降低;
  • 启用Constant Throughput Timer会提升重复率,禁用后重复率进一步降低(如50用户、200秒启动、无该定时器,45分钟执行803861次请求仅2次重复)。

注意:生成的数据不可随机,需遵循固定规则,因后续需上传至系统。

新旧逻辑示例

旧逻辑(计数器组件,Serialn随请求顺序递增,与BatchID无关)

Request 1:
BatchID = 100000000
Serialn = SN_100000000_0000

Request 2:
BatchID = 100001000
Serialn = SN_100001000_0001

Request 3:
BatchID = 100000000
Serialn = SN_100000000_0002

Request 4:
BatchID = 100001000
Serialn = SN_100001000_0003

新逻辑(Serialn按每个BatchID单独计数)

Request 1:
BatchID = 100000000
Serialn = SN_100000000_0000

Request 2:
BatchID = 100001000
Serialn = SN_100001000_0000

Request 3:
BatchID = 100000000
Serialn = SN_100000000_0001

Request 4:
BatchID = 100001000
Serialn = SN_100001000_0001

当前JSR223 Sampler脚本

SampleResult.setIgnore();

String product = vars.get("PRODUCT") //this is imported from a separate Counter, in the current setup, this counter changes the batchID
String batchID = String.format("%03d%03d%03d", Integer.parseInt(vars.get("MAH")), Integer.parseInt(product), 0);
String sCounter = props.get(product);
Integer counter = 0
if (sCounter != null) {
    counter = Integer.parseInt(sCounter);
}

StringBuilder builder = new StringBuilder();    
builder.append(String.format("<ns1:Pack sn=\"SN_%s_%04X\"/>", batchID, counter));
builder.delete(0,14);
builder.setLength(builder.length()-3);

counter ++;
props.put(product, String.valueOf(counter));

vars.put("Product.Scheme", "GTIN");
vars.put("Product.Code",vars.get("GTIN")); //this is a GTIN14 generated in a separate JSR223 Sampler
vars.put("Product.MAH_ID",vars.get("MAH"));
vars.put("Product.MAH_Name","LAST_MAH_"+vars.get("MAH"));
vars.put("Product.Batch.ID",batchID);
vars.put("Product.Batch.ExpDate", "251231");
vars.put("Product.Pack.SN.1",builder.toString());
vars.put("NHRN",vars.get("GTIN").substring(5, 13));

解决方案

问题根源是多线程并发读写JMeter全局属性props时存在竞态条件,多个线程会同时读取到相同的计数器值,最终生成重复的Serialn。以下两种方案可彻底解决该问题:

1. 给全局计数器添加同步锁

在读取和更新props中计数器的逻辑外包裹同步块,确保同一时间只有一个线程操作对应product的计数器:

SampleResult.setIgnore();

String product = vars.get("PRODUCT");
String batchID = String.format("%03d%03d%03d", Integer.parseInt(vars.get("MAH")), Integer.parseInt(product), 0);
Integer counter = 0;

// 针对当前product的锁对象,避免全局锁影响性能
synchronized (product.intern()) {
    String sCounter = props.get(product);
    if (sCounter != null) {
        counter = Integer.parseInt(sCounter);
    }

    // 生成SN并更新计数器
    String sn = String.format("SN_%s_%04X", batchID, counter);
    counter++;
    props.put(product, String.valueOf(counter));
    
    vars.put("Product.Pack.SN.1", sn);
}

// 非计数器相关的变量赋值(无需同步)
vars.put("Product.Scheme", "GTIN");
vars.put("Product.Code", vars.get("GTIN"));
vars.put("Product.MAH_ID", vars.get("MAH"));
vars.put("Product.MAH_Name", "LAST_MAH_" + vars.get("MAH"));
vars.put("Product.Batch.ID", batchID);
vars.put("Product.Batch.ExpDate", "251231");
vars.put("NHRN", vars.get("GTIN").substring(5, 13));

说明:

  • 使用product.intern()作为锁对象,确保同一product的计数器操作互斥,不同product之间互不阻塞;
  • 简化了原StringBuilder的冗余操作,直接生成目标SN字符串;
  • 同步块仅包裹计数器核心逻辑,其余操作放在块外提升执行效率。

2. 使用线程安全的Map存储计数器

利用Java原生的ConcurrentHashMap存储计数器,它本身支持原子更新操作,无需手动处理同步:

第一步:初始化线程安全计数器(放在Test Plan级别的Setup Thread Group中)

// 仅在测试启动时初始化一次
if (!props.containsKey("batchCounters")) {
    props.put("batchCounters", new java.util.concurrent.ConcurrentHashMap<String, Integer>());
}

第二步:修改业务JSR223 Sampler脚本

SampleResult.setIgnore();

String product = vars.get("PRODUCT");
String batchID = String.format("%03d%03d%03d", Integer.parseInt(vars.get("MAH")), Integer.parseInt(product), 0);

// 获取全局线程安全计数器Map
java.util.concurrent.ConcurrentHashMap<String, Integer> batchCounters = props.get("batchCounters");

// 原子更新计数器:不存在则初始化为0,返回更新前的旧值
Integer counter = batchCounters.compute(product, (k, v) -> v == null ? 0 : v + 1) - 1;

// 生成SN并赋值
String sn = String.format("SN_%s_%04X", batchID, counter);
vars.put("Product.Pack.SN.1", sn);

// 其他变量赋值
vars.put("Product.Scheme", "GTIN");
vars.put("Product.Code", vars.get("GTIN"));
vars.put("Product.MAH_ID", vars.get("MAH"));
vars.put("Product.MAH_Name", "LAST_MAH_" + vars.get("MAH"));
vars.put("Product.Batch.ID", batchID);
vars.put("Product.Batch.ExpDate", "251231");
vars.put("NHRN", vars.get("GTIN").substring(5, 13));

说明:

  • ConcurrentHashMap的compute方法是原子操作,从读取到更新计数器的全过程不会被其他线程打断;
  • 代码更简洁,无需手动维护锁,性能稳定性更高。

额外优化建议

  • 确保PRODUCT变量的生成逻辑线程安全,避免不同线程拿到相同的product值引发计数器冲突;
  • 若需持久化计数器(如测试中断后继续),可将计数器值写入文件,启动时读取初始值,更新时写入文件(需注意文件操作的线程安全);
  • 保持JSR223 Sampler的Cache compiled script if available选项启用,提升脚本执行性能。

内容的提问来源于stack exchange,提问作者Stefan Alexandru

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 00:35:00