1. 为什么参数化里的随机数环节最容易被低估
做性能测试的人几乎都绕不开 Jmeter,而只要涉及接口压测,参数化就是绕不过去的一道坎。很多人第一次接触 Jmeter 参数化,脑子里蹦出来的都是 CSV 文件、数据库取值这些"规规矩矩"的方案,反而把随机数、随机字符串这类看似简单的东西随手一带而过。结果到了真实压测现场,问题往往就出在这里:注册接口用同一个手机号跑了两千次,服务端全给你返回"该用户已存在";订单号用固定值提交,数据库唯一索引直接把一半请求拦在门外;上传接口的文件名不变,缓存命中率高得离谱,压出来的数据完全失真。
我自己早期带团队做压测的时候,就吃过这个亏。当时测一个优惠券领取接口,参数就是用户 ID 加券码,券码写死成一个固定字符串,跑出来的 TPS 高得吓人,我还挺得意,结果一看日志,服务端因为幂等校验把重复请求全短路了,真正打到核心逻辑的没几个。后来把券码改成随机字符串,TPS 直接掉了一大截——这才是真实的性能水位。从那以后,我对参数化里的随机数、随机字符串就格外上心,它不是什么高深技术,但它是决定压测结果"像不像真的"的关键一环。
这篇东西就是把我这些年用 Jmeter 做随机参数化的经验整理出来。核心关键词就三个:Jmeter、参数化、随机数、随机字符串。我会讲清楚 Jmeter 里生成随机数的几种主流方式怎么选、函数助手怎么配、JSR223 脚本怎么写、随机值怎么塞进请求体和数据库参数里,以及踩过的那些坑。适合谁看?刚上手 Jmeter 想搞明白参数化怎么玩的新人,以及做了一段时间但总觉得随机参数用得不顺手的老手。我不会只丢给你一段脚本,而是把"为什么这么做"也讲透,让你看完能直接抄作业,也能自己改。
先说一个我反复跟团队强调的观点:随机参数化的目的,从来不是"让数据看起来随机",而是让压测流量尽可能逼近真实生产流量。真实用户的手机号是随机的、订单号是唯一的、请求体里的字符串五花八门,你的压测脚本如果做不到这一点,测出来的数字就是自欺欺人。理解了这层,后面的所有操作和取舍才有判断依据。下面我按"选型逻辑—函数助手实操—脚本进阶—坑位排查"的顺序展开,每一步都附上我自己在项目里的真实操作记录。
2. Jmeter 生成随机数的几种路子与选型逻辑
2.1 函数助手:最省事的入门方案
Jmeter 自带的函数助手(Function Helper)是绝大多数人接触随机参数的第一个入口。打开方式很简单:菜单栏 Tools → Function Helper Dialog,或者直接在需要参数化的输入框旁边点开函数助手。里面跟随机数、随机字符串相关的核心函数就两个:__Random和__RandomString。
__Random的语法是${__Random(最小值,最大值,变量名)},比如${__Random(10000000,99999999,phoneSuffix)}就会在 8 位数字范围内生成一个随机整数,并存到phoneSuffix这个变量里供后续引用。__RandomString的语法是${__RandomString(长度,字符集,变量名)},比如${__RandomString(10,abcdefghijklmnopqrstuvwxyz0123456789,orderCode)}会生成一个 10 位的随机字符串。
它的优势是零代码、上手快、当天就能用,对刚入门的人极其友好。但你要清楚它的边界在哪:__Random生成的是整数区间内的随机值,__RandomString是从你给定的字符集里随机抽字符拼串。它能满足"值不重复、形态随机"的基本需求,但如果你要做有业务规则的随机数据——比如手机号必须以 13/15/18 开头、身份证号要符合校验位算法、金额要保留两位小数——函数助手就有点力不从心了。我的经验是:函数助手适合做"纯随机填充"的场景,比如随机订单号后缀、随机备注字段、随机文件名;一旦涉及业务校验规则,就换脚本方案。这个分界线,是我用了大量时间试错才划清的,新手照着用能少走不少弯路。
2.2 BeanShell 与 JSR223:让随机数带上业务逻辑
当随机值需要符合业务规则时,函数助手就不够了,得靠脚本处理器。Jmeter 里常见的有 BeanShell Sampler、BeanShell PreProcessor,以及更现代的 JSR223 Sampler、JSR223 PreProcessor。这里要划个重点:能选 JSR223 就不要选 BeanShell。
原因很实在。BeanShell 是一种解释型脚本,每次执行都要现场解析,性能开销大。在单线程小并发下你感觉不到差别,但当你模拟几百上千并发的时候,BeanShell 本身的执行耗时会混进你的测试结果里,把真实的接口响应时间污染掉。JSR223 支持 Groovy、JavaScript 等多种语言,其中 Groovy 是编译执行的,并且 Jmeter 会缓存已编译的脚本,重复执行时几乎没有解析开销。我做过一次对比,同样的随机数生成逻辑,JSR223+Groovy 比 BeanShell 在 500 并发下整体响应时间少了将近 15%,这个差距在性能测试里是致命的——你以为在测服务端,结果测的是脚本引擎。
所以选型逻辑就这么定:需要业务规则就用脚本,脚本首选 JSR223 Groovy。BeanShell 能看懂、能维护老脚本就行,新写的脚本一律 JSR223。这个判断标准我在团队里推行了两年,几乎没出过错。
2.3 三种方案的横向对比
为了让你一眼看清怎么选,我把三种方式整理成一张对照表:
| 方案 | 适用场景 | 优点 | 局限 | 推荐指数 |
|---|---|---|---|---|
__Random函数 | 纯整数随机填充 | 零代码、配置极快 | 无业务规则、仅限整数 | 简单场景首选 |
__RandomString函数 | 纯随机字符串填充 | 字符集可控、上手快 | 无格式约束、随机性一般 | 简单场景首选 |
| JSR223+Groovy | 带业务规则的随机数据 | 灵活、性能好、可复用 | 需要写代码、有学习成本 | 复杂场景首选 |
| BeanShell | 维护历史脚本 | 语法直观、上手易 | 性能差、已不推荐 | 仅维旧用 |
这张表是我自己踩坑总结的,不是什么官方文档。你在实际项目里可以照这个逻辑走:先用最简单的函数助手快速跑通,遇到规则约束再升级到 JSR223,基本不会走错路。
3. 从零实操:随机参数化的完整落地过程
3.1 先把数据规范想清楚,别急着开工具
很多人一上来就打开 Jmeter 猛写脚本,结果写到一半发现字段长度、字符集、格式都没定,来回返工。我现在的习惯是:动手前先列一张"数据规范表"。拿注册接口举例,你至少要明确几个东西:手机号的位数和号段前缀、用户名的长度范围和允许字符、密码的复杂度要求、邮箱的格式、订单号的唯一性要求。这些不是拍脑袋定的,而是从接口文档和数据库字段定义里抄出来的。
注意:数据规范一定要跟接口文档、数据库约束对齐,否则随机数据再"随机"也会被服务端校验拦掉。我曾经因为忽略了数据库里手机号字段的 unique 约束,随机生成了几万条数据,结果一半重复,白白浪费了一轮压测。
把规范定下来之后,再决定每个字段用函数还是用脚本。我的经验是,一张表里通常 70% 的字段用函数助手就够了,只有那几个有强业务规则的字段才需要脚本。这样既保证了质量,又不至于把自己累死。
3.2 函数助手配置的现场记录
以最常见的注册压测为例,我实际操作是这样的。先打开函数助手,在 Function 下拉框里选__RandomString,然后在参数框依次填:串长度填 8,字符集填abcdefghijklmnopqrstuvwxyz0123456789,变量名我习惯叫userName。点击 Generate & Copy to Clipboard,它会生成类似${__RandomString(8,abcdefghijklmnopqrstuvwxyz0123456789,userName)}的表达式。把它粘到 HTTP Request 的用户名参数值里就行。
手机号我是这么处理的:手机号一般是 11 位,前 3 位固定号段,后 8 位随机。所以我用两段拼起来——前 3 位写死成138,后面接${__Random(10000000,99999999,phoneSuffix)}。最终表达式长这样:138${__Random(10000000,99999999,phoneSuffix)}。这样既保证了号段合规,又保证了足够大的随机空间。
这里有个特别容易忽略的细节:函数取值时机。Jmeter 里的函数默认在每次执行到它所在的元件时求值,但如果你在测试计划里勾选了某些作用域设置,或者函数被放在配置元件里,它的求值时机就不一样了。我踩过最典型的坑是:把随机函数放在 CSV Data Set Config 的同一层级,结果它只求了一次值,所有线程用的是同一个随机数。解决办法是把函数直接写在请求参数里,让它在每次请求时重新求值。这个点新手几乎必踩,记牢它。
3.3 JSR223 脚本生成带规则的随机数据
现在讲脚本方案。在需要参数化的 Sampler 上右键,添加 → 前置处理器 → JSR223 PreProcessor,Language 选 Groovy,然后写脚本。下面这段是我做订单号生成时常用的模板,你可以直接抄:
import java.text.SimpleDateFormat // 生成带时间戳的订单号:日期 + 6位随机数字 def sdf = new SimpleDateFormat("yyyyMMddHHmmss") def datePart = sdf.format(new Date()) def random = new Random() def numPart = String.format("%06d", random.nextInt(1000000)) def orderNo = "ORD" + datePart + numPart vars.put("orderNo", orderNo) log.info("本次生成的订单号:" + orderNo)这段脚本有几个设计考虑值得说。第一,用时间戳保证前缀单调递增,避免大量请求在极短时间内撞号;第二,6 位随机数字提供 100 万的空间,配合时间戳基本杜绝重复;第三,vars.put("orderNo", orderNo)是把值塞进 Jmeter 变量,后续用${orderNo}就能引用。vars是 Jmeter 提供的一个线程级变量容器,每个线程有自己独立的一份,天然做到了线程隔离——这是脚本方案比函数助手更可控的地方。
生成随机字符串时,我有时需要指定字符集和长度,脚本是这样:
def chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" def random = new Random() def sb = new StringBuilder() def length = 12 for (int i = 0; i < length; i++) { sb.append(chars.charAt(random.nextInt(chars.length()))) } vars.put("randomStr", sb.toString())注意事项:Groovy 脚本里不要用
Math.random()这种老写法去截取,容易踩精度和格式的坑;用Random或ThreadLocalRandom更稳。另外,脚本里少做复杂运算,能在预处理阶段算完的就别放到请求线程里,避免污染响应时间统计。
3.4 把随机值塞进请求体和数据库参数
生成出来只是第一步,关键是让它真正生效。对于 HTTP 请求,在 Body Data 或 Parameters 里用${变量名}引用即可。如果请求体是 JSON,那就写成{"userName":"${userName}","phone":"${phone}"}这样的形式,Jmeter 会在发送前完成变量替换。
数据库参数化稍微绕一点。用 JDBC Request 时,如果参数要动态传入,常见做法是把随机值先通过 JSR223 存进vars,然后在 JDBC Request 里用${变量名}引用,参数类型选对应的类型。另一种方式是把随机值写进 CSV,再用 CSV Data Set Config 读取,适合数据量特别大的场景。这里有个坑:JDBC 参数如果是字符串类型,千万注意引号和类型匹配,字符串参数在某些数据库驱动下需要显式加单引号,数字参数则不能加,否则会报类型错误。我在测 MySQL 时就因为把随机数字当成字符串传,报过Data truncation的错误,排查了半天才发现是类型问题。
4. 常见坑与排查技巧实录
4.1 随机数重复、分布不均怎么办
随机数重复是压测里最让人头疼的问题之一。表面上看随机数空间很大,但如果你用的范围太小,比如只生成 1 到 100 的随机数跑一万次,那碰撞概率高得吓人。判断方法很简单:把生成的随机值写到日志或结果文件里,用脚本统计一下重复率。
解决思路分两层。第一层是扩大随机空间,手机号用完整 8 位而不是 3 位,订单号加上时间戳和随机位。第二层是如果业务要求绝对唯一,那就别纯靠随机,改用"时间戳+线程号+随机数"的组合模式,或者干脆用AtomicLong做自增计数配合随机前缀。我在一次高并发压测里就用了组合方案:单个线程内自增保证不重复,跨线程用随机前缀区分,最终唯一性达到 100%。
4.2 函数取值时机与线程共享的坑
这个坑前面提过一句,这里展开说透。Jmeter 的函数在测试计划启动时、元件执行时、还是每次请求时求值,取决于它被放在哪里、以及是否配置了缓存。默认情况下,函数是"按需求值",放对位置就没问题。但如果你不小心把随机函数放进了配置元件(比如 User Defined Variables),那它只会在初始化时求值一次,所有线程共享同一个值——压测结果全废。
排查方法:在函数表达式后面加个log.info打印,或者直接在察看结果树里看不同线程发出去的参数值是否相同。如果全一样,百分之百是求值时机问题。避坑技巧:随机参数一律直接写在 Sampler 的参数里,不要放配置元件;如果确实需要共享,就明确接受共享的后果,别混着用。
4.3 随机字符串乱码与长度问题速查
随机字符串最常出的两个问题,一个是乱码,一个是长度不对。乱码通常是因为字符集里混入了中文或特殊符号,而接口或数据库的编码是 UTF-8,传输时对不上,就会出现问号或者奇怪字符。解决方式是随机字符串的字符集尽量限定在 ASCII 可见字符范围内,实在需要中文就用明确的 Unicode 转义。
长度不对多半是拼接逻辑有问题。比如长度参数写错了位,或者循环次数和长度变量搞混了。我整理了一张速查表:
| 问题现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 随机值全部相同 | 函数放在配置元件里 | 看结果树不同线程参数 | 移到 Sampler 参数中 |
| 随机数重复率高 | 随机范围太小 | 统计结果文件重复率 | 扩大范围或用组合方案 |
| 字符串乱码 | 字符集含非 ASCII | 检查字符集定义 | 限定可见字符范围 |
| 长度不符 | 拼接或循环逻辑错 | 打印中间变量 | 修正循环次数和长度 |
| JDBC 参数报错 | 类型与引号不匹配 | 看数据库错误日志 | 按类型调整引号 |
提示:排查随机参数问题时,第一步永远是"看实际发出去的值",而不是猜。察看结果树里的请求体,或者加个 debug 日志,比任何推断都快。
5. 进阶玩法与性能取舍
5.1 大数据量与 CSV 参数化的配合
当随机数据的量级特别大,比如要模拟百万级用户,纯靠脚本实时生成可能有性能压力。这时候一个折中方案是"预生成+随机读取":提前用脚本生成一个十万条随机数据的 CSV 文件,在压测时用 CSV Data Set Config 随机抽取。Jmeter 的 CSV 读取默认是按行顺序取,多线程时会做分块,每个线程读一段,这样既保证了数据多样,又避免了实时计算的成本。
具体操作:CSV Data Set Config 里 Filename 指向文件,Variable Names 填列名,Sharing Mode 根据需求选。如果要每个线程用不同的数据块,就选 "Current thread group",如果所有线程共享读取游标,就选 "All threads"。这个选择直接影响数据利用率,选错会导致某些线程的数据序列和其他线程高度重合,降低随机性。
5.2 随机参数对测试报告解读的影响
最后说一个容易被忽视的点:随机参数会改变你的报告解读逻辑。用固定参数压测,服务端可能因为缓存、幂等、连接复用等原因给出偏高的性能表现;换成随机参数后,请求要真正走一遍完整逻辑,TPS 通常会更低,但更接近生产真实情况。所以你在对比历史报告时,必须先确认参数化方案是否一致,否则数字没有可比性。
我的经验是,正式压测前先跑一轮小并发(比如 20 线程)的随机参数冒烟,确认数据能正常通过服务端校验,再逐步加压。这个习惯帮我省下了无数次"跑到一半发现数据全被拦"的返工。另外,随机参数会让响应时间的分布更离散,看报告时多关注 90%、95% 分位值,别只盯着平均值,你会发现随机参数下的长尾请求比固定参数明显,这才是真实用户会遇到的体验。
说到最后,分享一个我自己养成的习惯:每次新建压测脚本,先花五分钟把参数规范写成注释贴在测试计划顶部,再动手配置。看起来多此一举,但真正做到之后,脚本的可维护性和可复现性会提升一大截。随机参数化这件事,难点从来不在工具本身,而在于你有没有把"模拟真实流量"这个目标真正放进心里。工具只是手段,思路才是关键。