我们遇到了正则表达式的灾难性回溯问题

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) 策略:

  1. 贪婪优先 :量词(*/+)总是先尝试匹配最长可能
  2. 逐层试探 :当后续匹配失败时,引擎会回溯到上一个有选择的点
  3. 穷尽所有路径:直到找到成功路径或所有路径失败

二、(\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* 的匹配过程(无回溯):

  1. 从位置 0 开始
  2. 贪婪匹配所有数字直到结束
  3. 一次完成,无分支选择

✅ 关键 :\d* 没有内部结构,不需要考虑"如何分组",因此不会产生回溯

五、真实案例:你的代码为何卡死

如果不修改正则表达式:

java 复制代码
(-?\\d+(\\,\\d+)*(\\.)+(\\d+)*/-?\\d+(\\,\\d+)*(\\.)+(\\d+)*)

存在 三重灾难组合:

  1. 嵌套量词 :(\d+)* 出现两次
  2. 长输入 :300+ 个 0 的字符串
  3. 匹配失败 :字符串不含 /,整个匹配必然失败
计算回溯次数:
  • 仅考虑 (\d+)* 部分:2^(300-1) = 2^299 ≈ 10^90 次尝试
  • 作为对比:宇宙原子总数 ≈ 10^80
  • 假设 CPU 每纳秒处理 1 万次尝试:
    所需时间 = 10^90 / (10^4 * 10^9) 秒 ≈ 3 × 10^73 年

宇宙爆炸都执行不完!!!

相关推荐
EatFan9 小时前
Spring Boot 4 落地观察:从 yudao-cloud、matecloud、JPower 看国产脚手架的升级路线与迁移清单
java·spring boot·后端·spring cloud·微服务·后端开发·jdk 21
Maiko Star10 小时前
* LangChain 提示词模板详解:ChatPromptTemplate 的使用与高级特性
java·人工智能·langchain
现任明教教主~12 小时前
Thinkphp站群蜘蛛池SaaS系统YanyvSEO含多用户/积分/六大引擎计费/易支付对接
java·开发语言·spring
一条破秋裤13 小时前
Linux 线程创建:pthread_create 与基本回收
java·linux·运维
布吉岛的石头13 小时前
Java 程序员第 49 阶段5:BERT 预训练目标 MLM+NSP 的工程含义
java·人工智能·深度学习·bert·transformer
摇滚侠14 小时前
《On Java 中文版 基础卷》阅读笔记 对象无处不在 03
java·笔记·python
二十雨辰14 小时前
[学成在线]-01项目背景
java
弈栈录15 小时前
Java AI 应用的异步化与高并发设计
java·后端·架构
落魄实习生15 小时前
Agent Scope Java 2.x 系列【2】 ReActAgent
java·开发语言
艸肅15 小时前
Ubuntu 安装 JDK (含手动安装)
java·ubuntu·jdk