1、现象
用户反馈某个功能一直卡住,导致不能正常使用系统。

2、定位
经过排查,发现相关线程已经把cpu打满。

查看到有问题的具体代码位置,发现是在使用正则匹配的时候出现问题了。

相关正则匹配代码如下:
java
text.matches("(-?\\d+(\\,\\d+)*(\\.)+(\\d+)*/-?\\d+(\\,\\d+)*(\\.)+(\\d+)*)");
text字符串为用户传入的文本。
怀疑是进入了死循环,但是查看完相关代码的上下文后,并没有写可以造成死循环的代码片段。
3、原因
后面经过线下写demo脚本,拉取当时处理的数据,在本地跑这个正则匹配,终于发现了一条有问题的数据。此处只粘贴了用户传入文本的数字部分。
java
String text = "0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
经过仔细查看正则表达式,发现里面出现了(\d+)*这种匹配格式,然而这就是典型的回溯陷阱,如果传入的数字足够长,会导致指数级的回溯,最终引发回溯爆炸。
(\d+)*是灾难性写法 :\d+已经表示"一个或多个数字",外面再套*(零次或多次)------这等价于\d*,但会让正则引擎尝试无数种分割方式(比如把 "123" 拆成 ["123"], ["12","3"], ["1","23"], ["1","2","3"]...),造成 回溯爆炸。
4、处理
既然已知道**(\d+)*这种写法其实等价于** \d*,那么就把它替换掉即可。
替换后的正则表达式:
java
text.matches("(-?\\d+(\\,\\d+)*(\\.)+\\d*/-?\\d+(\\,\\d+)*(\\.)+\\d*)");
5、为什么(\d+)* 这种模式,是经典的回溯陷阱
一、正则引擎的匹配本质:试探与回溯
正则表达式引擎(特别是 Java/Python/JS 等使用的 NFA 引擎 )在匹配时采用 深度优先搜索(DFS) 策略:
- 贪婪优先 :量词(
*/+)总是先尝试匹配最长可能 - 逐层试探 :当后续匹配失败时,引擎会回溯到上一个有选择的点
- 穷尽所有路径:直到找到成功路径或所有路径失败
二、(\d+)* 的致命结构分解
让我们拆解这个模式:
( \d+ )*
2↑ ↑ ↑
3A B C
- A:捕获组(增加状态记录开销)
- B :
\d+→ 可变长度匹配(1~n 个数字) - C :
*→ 可变次数重复(0~m 次)
⚡ 核心问题:双可变性叠加
- 每次重复时,
\d+自身有多种匹配长度选择 *量词又允许任意重复次数- 二者结合 → 组合爆炸(Combinatorial Explosion)
三、实战演示:字符串 "123" 的匹配过程
假设正则:^(\d+)*$(全匹配数字串)
虽然最终会成功,但过程暴露了问题。匹配失败时问题更严重
步骤 1:贪婪匹配(首选路径)

✅ 1 次尝试就成功
但!当正则包含后续内容且匹配失败时(灾难场景)
正则 :^(\d+)*X$
输入 :"123"(没有 X)
🔍 具体尝试路径(共 4 种有效分割):
| 尝试顺序 | 分割方案 | 后续匹配 X | 结果 |
|---|---|---|---|
| 1 | ["123"] |
失败 | 回溯 |
| 2 | ["12", "3"] |
失败 | 回溯 |
| 3 | ["1", "23"] |
失败 | 回溯 |
| 4 | ["1", "2", "3"] |
失败 | 最终失败 |
💥 关键发现 :长度为 n 的数字串,有 2^(n-1) 种分割方式!
- n=3 → 4 种(2²)
- n=10 → 512 种
- n=30 → 536,870,912 种(5亿+次尝试!)
四、与安全模式的对比:为什么 \d* 没问题?
(\d+)* vs \d* 的本质区别
| 模式 | 匹配逻辑 | 决策点数量 | 时间复杂度 |
|---|---|---|---|
(\d+)* |
"分割字符串为多组数字" | O(2^n) | 指数级 |
\d* |
"匹配任意长度的连续数字" | O(n) | 线性 |
\d* 的匹配过程(无回溯):
- 从位置 0 开始
- 贪婪匹配所有数字直到结束
- 一次完成,无分支选择
✅ 关键 :
\d*没有内部结构,不需要考虑"如何分组",因此不会产生回溯
五、真实案例:你的代码为何卡死
如果不修改正则表达式:
java
(-?\\d+(\\,\\d+)*(\\.)+(\\d+)*/-?\\d+(\\,\\d+)*(\\.)+(\\d+)*)
存在 三重灾难组合:
- 嵌套量词 :
(\d+)*出现两次 - 长输入 :300+ 个
0的字符串 - 匹配失败 :字符串不含
/,整个匹配必然失败
计算回溯次数:
- 仅考虑
(\d+)*部分:2^(300-1) = 2^299 ≈ 10^90 次尝试 - 作为对比:宇宙原子总数 ≈ 10^80
- 假设 CPU 每纳秒处理 1 万次尝试:
所需时间 = 10^90 / (10^4 * 10^9) 秒 ≈ 3 × 10^73 年
宇宙爆炸都执行不完!!!