
1. 随机参数在压测脚本里到底扮演什么角色做接口压测的人大概都遇到过这样一种尴尬脚本在本地跑单个请求时一切正常一上到几十上百并发服务端就开始报订单号已存在或者手机号重复。排查半天发现不是代码有 bug也不是服务扛不住而是请求体里那个字段从头到尾写死成了同一个值。这就是参数化没做到位。JMeter 的参数化手段很多CSV 数据文件、用户定义的变量、数据库取值都算但生成随机数和随机字符串是其中最轻量、也最容易被误用的一类。轻量是因为它不需要额外准备数据文件一个函数就能搞定容易被误用是因为很多人只记住了函数长什么样却没搞清楚它什么时候求值、作用域在哪、能不能被复用结果随机出来的是看起来随机、实际上每次都一样的假数据。这篇内容面向的是已经能把 JMeter 跑起来、大致知道取样器和线程组怎么用但在参数化细节上频繁踩坑的同学。我会把__Random、__RandomString、Random Variable 配置元件、__UUID、__time这几条原生路线挨个拆开讲清楚边界再补上 JSR223 Groovy 的定制玩法最后重点聊一聊那些文档里不写、只有真正压过测的人才知道的坑。目标很明确看完你能自己判断某个字段该用哪个函数、该不该存变量、多线程下会不会撞车而不是照着别人的截图抄一遍了事。1.1 从一次订单号重复的压测翻车说起我印象最深的一次是给一个下单接口做 200 并发的压测。脚本里订单号字段当时写的是固定值TEST20240101001本地单线程跑通了就直接进了压测。结果压测一开始服务端返回的错误里有一半是数据库唯一索引冲突。当时第一反应是是不是服务端并发写有问题查了日志才发现是我们自己每次都拿同一个订单号去撞唯一键。问题不在被测系统在脚本。后来把订单号换成${__UUID()}同一份脚本立刻干净了。这个例子说明一个很朴素的道理压测请求要和真实用户行为在数据多样性上尽量贴近。真实用户不会用同一个手机号注册两次不会用同一个订单号下单所以脚本如果全用固定值你压出来的要么是唯一的冲突路径要么是缓存命中的假象根本测不出真实负载下系统的表现。随机参数化就是用来补齐这块短板的。1.2 随机、唯一、可变三个容易混淆的概念在动手之前得先把三个词分开随机、唯一、可变。它们经常被当成一回事其实诉求完全不同选型也因此不同。随机每次取值从某个范围或字符池里挑一个可能重复也不保证分布均匀。唯一要求全局或线程内不出现重复值__UUID才是干这个的__Random靠不住。可变只要两次请求不完全一样就行弱于唯一随机字符串基本能满足。这三者对应到具体场景是这样的注册接口的手机号最好偏唯一因为重复手机号会命中已注册分支而像商品备注、搜索关键词这类字段用随机或可变就够了重复了也没人管。至于订单号、流水号这种带业务约束的既要唯一又要符合格式就得靠自定义脚本去拼。把它们想成装修选材随机是随手抓一把不同颜色的瓷砖唯一是每块瓷砖都刻了不同的编号可变只是要求别整面墙都是同一种颜色。需求没想清楚抓错工具后面的坑就等着你了。提示判断字段属于哪一类最快的办法是问自己一句这个字段在数据库里有没有唯一约束。有就往唯一方向选没有随机和可变随便挑。2. JMeter 四种原生随机生成手段的适用边界JMeter 自带的函数和元件不少但真正用来造随机数据的其实就那么几个。很多人一上来就挑花眼或者干脆一个__Random走天下结果在需要字符串的地方硬塞数字、在需要唯一的地方硬用随机最后返工。这一节把四种原生手段的适合干什么、不适合干什么摆清楚。2.1 __Random 函数整数随机的默认答案__Random是 JMeter 内置函数里最常用的一個语法是这样的${__Random(最小值,最大值,存储变量名)}比如${__Random(1,100,randNum)}就是在 1 到 100 之间取一个整数同时把结果存进randNum这个变量后面用${randNum}引用就行。第三个参数是可选的不写就直接把结果内联到请求里。它的适用场景很明确年龄、数量、页码、ID 段这类纯数字字段。像分页查询里的pageSize、pageNum或者下单接口里的quantity用__Random几秒钟就能搞定。但这里有两个必须提前知道的细节。第一区间边界到底是闭区间还是开区间不同 JMeter 版本行为不完全一致。如果你后面要拿这个随机值做精确断言先在本地单线程多跑几次确认最大最小值能不能取到别想当然。第二最大值必须大于最小值否则函数会返回异常值或者直接报错。我见过有人把参数顺序记反写成${__Random(100,1,...)}脚本不报错但生成的数全是乱的排查起来很费劲。还有一个容易忽略的点__Random生成的是整数。想要小数它不是设计来干这个的得靠 Groovy 或者把随机数除一下再拼接别在这上面硬凹。2.2 __RandomString 函数字符池决定一切字符串随机得靠__RandomString语法${__RandomString(长度,字符池,存储变量名)}比如${__RandomString(8,abcdefghijklmnopqrstuvwxyz,randStr)}生成一个 8 位的纯小写字母字符串并存进randStr。字符池你可以随便组合想要数字就写0123456789想要字母数字混合就写全想避开容易混淆的0O1lI就把它们剔出去。这里的核心认知是这个函数的随机质量完全取决于你给的字符池。很多人图省事不写字符池指望 JMeter 给个默认值结果不同版本下行为不一样有的能跑有的直接报错。所以我的习惯是永远显式指定字符池别依赖默认。哪怕你只是想生成一段无意义的备注也老老实实把池子写出来。第二个细节是长度。长度设成 6 还是 20直接决定碰撞概率。如果你有一个 100 万级的用户池生成长度 6 的纯数字字符串碰撞几乎必然发生这时候要么加长要么换__UUID。字符池大小乘以长度决定了理论空间心里大概有个数。2.3 Random Variable 配置元件作用域比生成逻辑更值得研究除了函数JMeter 还提供一个Random Variable配置元件在 Config Element 里能找到。它长得很像__Random有变量名、输出格式、最小值和最大值。但它有一个__Random没有的关键开关Per Thread (User)。这个开关决定了随机值的作用域也是这个元件最值得研究的点设置行为适用场景勾选 Per Thread每个线程各生成一次线程内该值固定不变想要每个虚拟用户身份固定的字段如用户 ID不勾选 Per Thread所有线程共享同一个值全局只生成一次极少用多数情况下反而会造成数据集中看出来了吗Random Variable 有一种__Random做不到的能力让某个随机值在整个线程生命周期里保持不变。比如你想模拟某个固定用户反复操作那这个用户的 ID 就该在一次次循环里保持稳定这时候 Random Variable 勾上 Per Thread 就非常合适。用__Random反而会在每次引用时重新取值伪装不成固定用户。它的输出格式字段用的是java.text.DecimalFormat规则。写000会补零成三位比如生成 7 会变成007写000000就是六位流水号的感觉。要生成带前导零的业务编号这里比函数方便得多。2.4 __UUID 与 __time唯一性诉求下的平替方案当你需要的其实是唯一而不是随机__UUID就是最省心的选择${__UUID()}它直接吐出一个标准 36 位 UUID碰撞概率低到可以忽略非常适合订单号、请求追踪 ID 这类字段。用法上没什么讲究缺点是格式不美观——一长串十六进制加横杠如果业务对编号格式有要求就不能直接用得拿它做种子再拼。__time则是另一条路${__time()} 生成当前毫秒时间戳 ${__time(yyyyMMddHHmmss)} 按格式生成时间字符串时间戳天然带单调递增的性质配合线程号或者随机数可以拼出一种既唯一又好看的编号。经典配方是${__time()}${__threadNum}${__Random(1000,9999)}时间戳保证时间维度不重复线程号保证并发维度不重复尾部的随机数兜底。三个拼起来在常规压测规模下几乎不会撞。还有个小众但好用的__RandomDate用来生成随机日期做注册时间、活动时间这类字段时能省不少事格式同样是自定义的。注意__UUID和__time生成本质上都不是随机它们强在去重。如果你的字段其实只需要看起来不一样没必要上 UUID随机字符串更轻。3. 函数写法里的细节参数、转义与求值时机函数本身不难难的是把它们塞进真实请求时的那些细节。参数顺序、求值时机、变量存取任何一个没搞明白脚本表现都会和你预期的不一样。这一节专门抠这些差之毫厘的地方。3.1 参数顺序搞反会静默生成错误数据JMeter 的函数参数是按位置传的没有参数名保护。__Random(1,100,var)和__Random(100,1,var)在编辑器里看起来差不多结果一个正常一个离谱。更麻烦的是参数错了不一定报错它会安安静静给你返回一个不对劲的值你在结果树里不看请求体根本发现不了。我的做法是写完函数一定打开 Function Helper Dialog选项菜单里能调出来。这个小工具可以直接填参数、点生成它帮你拼出正确的函数字符串省得手写出错。第一次用某个函数时先在这个对话框里生成一遍看清最终格式再往脚本里贴。另外多个函数叠在一起的时候括号和逗号特别容易错。比如${__RandomString(6,${__Random(10,99)},v)}这种嵌套肉眼很难确认对不对。这种时候宁可按部就班先分步生成、存进变量再拼起来别贪那点简洁。3.2 ${} 的求值时机与嵌套函数${}是 JMeter 的变量/函数引用语法。函数写在${}里每次这个引用被采样器执行到时都会重新求值。也就是说${__Random(1,100)}直接写进请求体那么每个线程、每次循环它都会重新取一个新随机数这就是我们想要的效果。但如果你希望某个值只算一次之后一直用就必须用第三个参数把它存进变量后面引用变量名${__Random(1,100,myRand)} ${myRand}${myRand}只是取值不会重新随机。这两者的区别是把随机逻辑用对的关键。很多人抱怨我明明用了随机但它每次一样八成就是把随机值存进了变量然后反复引用变量本身而变量里的值在说出那句存一次之后就没再动过。至于嵌套${}支持函数套函数但求值是从里往外、并且第一层之外通常只在引用变量时展开。复杂的嵌套建议拆开可读性优先——压测脚本以后还要维护别写成解谜游戏。3.3 把一次性随机值固定下来变量的存取变量的存取看起来简单但有个细节值得单独说JMeter 变量是线程私有的。你在 A 线程里vars.put(x, ...)存的值B 线程读不到。这既是限制也是优势。优势在于如果你想让每个虚拟用户有自己的一套随机身份直接把值存进变量就行天然按线程隔离不用担心互相污染。限制在于如果你想跨线程共享一个值比如全局唯一的资源池就得换属性properties或者外部存储变量做不到。${__Random(1,100,varName)}里的varName存的就是线程变量行为和我上面说的一致。想清楚你要的是每个用户一份还是全局一份选型自然就出来了。4. 内置函数不够用时的 Groovy 定制路线内置函数覆盖了八成场景但总有两成它搞不定带校验位的身份证号、特定前缀的业务编号、符合某个正则的邮箱。这时候就得自己写脚本。JMeter 里能干这事的有 BeanShell 和 JSR223现在主流选择是后者配 Groovy。4.1 为什么 JSR223 Groovy 取代了 BeanShell早些年大家用 BeanShell 写脚本但 BeanShell 每次执行都要解释一遍脚本一多、并发一高脚本本身就成了性能瓶颈甚至会把压测结果带偏——你压的到底是服务端还是脚本引擎说不清楚。JSR223 Groovy 是编译执行的脚本会被缓存复用性能好一个量级。所以现在新写脚本默认就走 JSR223 Sampler 或者 JSR223 PreProcessor Groovy 语言。一个典型的随机手机号 PreProcessor 长这样import java.util.Random def random new Random() // 第二位常用 3-9这里简化处理 def prefix 1 (3 random.nextInt(7)) def suffix String.format(%08d, random.nextInt(100000000)) vars.put(phone, prefix suffix)挂好后请求体里直接用${phone}引用就行。注意vars.put存的是线程变量符合前面说的线程隔离原则。提示写 Groovy 的时候用import显式引入类别依赖默认导入生成的随机值一定要vars.put出来否则脚本结果你拿不到。4.2 有格式约束的随机数据怎么造有格式的随机数据才是脚本存在的主要理由。举几个我实际用过的例子邮箱user_ random.nextInt(1000000) test.com前缀固定、数字随机简单够用。带前缀的订单号ORD System.currentTimeMillis() String.format(%04d, random.nextInt(10000))时间加随机兼顾唯一和可读。身份证号如果业务只校验长度和格式不校验校验位用随机数字拼 18 位即可如果对方系统真在校验就得按规则算校验位这个用 Groovy 写个函数也不难但一定要问清楚被测系统的校验强度别做无用功。这里最容易被忽略的是被测系统的实际校验规则。我们有一次压一个手机号必须符合运营商号段的接口脚本随手用1打头接九位随机数字结果一批请求被接口直接拒掉压出来的成功率全线飘红。后来才发现接口对号段有白名单校验。所以动手前把接口文档里对字段的约束条件翻出来对一遍比写十行脚本都值。4.3 RandomUtils 与 java.util.Random 的取舍JMeter 的 lib 目录里已经带了 Apache Commons Lang所以org.apache.commons.lang3.RandomUtils可以直接用import org.apache.commons.lang3.RandomUtils def num RandomUtils.nextInt(1000, 9999) // 整数 def str RandomStringUtils.randomAlphanumeric(8) // 字母数字混合 def digits RandomStringUtils.randomNumeric(6) // 纯数字RandomStringUtils的优势是语法简洁randomAlphanumeric、randomNumeric、randomAlphabetic这几个方法名一看就懂省得自己拼字符池。那java.util.Random什么时候更合适当你需要固定种子、可复现的随机序列时。new Random(12345)给定种子每次生成的序列是固定的这在排查一个只在特定数据上才出现的 bug 时非常有用——你能反复造出同一批随机数据。需要复现用java.util.Random需要随手造数据用RandomStringUtils两者不冲突按场景挑。5. 随机参数与真实脚本的组合打法单个函数会用了不等于能把随机参数用好。真实的压测脚本里随机值往往要和 CSV、断言、关联混在一起工作这时候新的问题就冒出来了。5.1 随机 CSV数据集之上的扰动层CSV Data Set Config 是 JMeter 里最常用的参数化方式它按线程、按迭代逐行取数据。但它有个特点同一行数据在整个迭代里是固定的一整套。有时候你希望基础数据来自 CSV但某些字段每次再抖一抖这就需要随机函数来补。典型场景CSV 里存了用户名和密码登录要真实账号但每次下单的商品数量、备注想随机。这时候登录用${username}取自 CSV下单数量用${__Random(1,5)}现算。CSV 提供稳定的身份随机函数提供变化的噪声两层叠加脚本更像真实用户。要注意的是配置元件的执行顺序。CSV Data Set Config 每迭代取一行而随机函数在每次被引用时求值两者的节奏不一样。如果你期望每个用户从 CSV 拿一行后整个迭代里数量都固定那就得把随机值存进变量再引用而不是每次都内联函数。搞清楚谁在哪一刻求值组合才不会乱。5.2 多线程下的随机值分布与线程安全单线程跑随机怎么都对上了多线程就得关心分布和碰撞。几个经验点__Random在多线程下是各线程独立取值的不会有线程安全问题但范围太小时碰撞会很明显。比如 10 个线程都在 1 到 5 之间取数那基本就是挤在一起。范围要开到远大于并发数。Random Variable 不勾 Per Thread 时是全局只生成一次所有线程拿到的都一样多数场景这是坑不是特性别手滑。__UUID天然线程安全且几乎不撞属于最省心的唯一方案。自定义 Groovy 里如果用了共享的可变对象就要留意线程安全。new Random()放在脚本里每次新建是安全的但如果有人为了提高性能把它提到线程组外面共享那就得当心并发了。想验证分布最简单的办法是加个监听器把生成的字段记下来跑一轮小并发看看结果树里的取值是不是分散的。分布太集中要么扩大范围要么加长字符串长度。5.3 让断言跟上随机参数的变化参数化改了断言最容易跟着出问题。比如你原本断言响应里包含固定的请求 ID换成随机 ID 后这条断言必然失败。这时候要改用变量引用或正则关联用__Random存变量后断言里引用${myRand}动态匹配。用 JSON 断言时用 JSONPath 或正则去匹配响应中的对应字段而不是写死值。常见做法是在请求后加一个 JSON 提取器把服务端返回的字段提取出来再断言它和请求一致。随机参数让请求值变得不可预测断言就必须从对比固定值转向对比关联值。这一步不做你的压测报告里会满是假失败。6. 排错随机参数引发的诡异现象与定位手法最后这部分专门收集那些让人血压升高的现象以及我踩过之后总结的定位路径。6.1 结果树看不到随机值现象请求体里明明写了${__Random(1,100)}结果树里看到的却是原样的${__Random(1,100)}字符串。原因通常是引用语法写错了例如漏了$、漏了花括号或者写成了$(...)。JMeter 只认${...}别的形态一律当普通文本处理不会求值。排查顺序先检查${}是否完整再确认函数名拼写正确大小写敏感__Random是两个下划线然后用 Function Helper Dialog 生成一遍对比。还有一种是值确实生成了但没显示——那是监听器的时机问题换个查看结果树的取样器看看。6.2 随机撞号导致的接口报错现象压测中偶发数据已存在错误单跑没有。多半是随机值空间太小高并发下撞了。定位办法在脚本里把生成的随机值记录到结果文件加个响应断言或用一个 Debug Sampler 输出变量跑一轮看有没有重复。解决路径有两级一是加大随机空间长度或范围往上提二是从随机切换到唯一方案比如__UUID或者时间戳 线程号 随机的组合。前者省事后者彻底。判断标准还是那句话——字段有没有唯一约束。6.3 报告里请求差异过大如何归因现象聚合报告里响应时间抖动很大怀疑是随机参数导致了服务端命中不同分支。这时候别急着下结论先把随机字段的取值导出分析。JMeter 的察看结果树能导出或者直接在脚本里把关键参数写进结果文件。我的做法是压测前就在脚本里加一个参数快照——把本次请求用到的随机值一并输出到日志或结果文件。这样报告一旦异常可以直接对着参数看是不是某些取值命中了慢查询、是不是某些参数触发了缓存失效。没有这个快照你只能对着一个孤零零的响应时间数字猜效率差太多。这个思路其实就是把随机从黑盒变成白盒。随机参数本身很简单难的是你永远不知道这次跑的是哪一组随机值所以主动把随机值记录下来是让压测结果可解释的前提。我现在的脚本里凡是用了随机或唯一的字段基本都会顺手输出一份回看的时候省了太多事。真要长期跑压测这个习惯建议早点养成。