如何避免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
相关产品推荐
相关产品推荐

