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

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 年

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

相关推荐
Meteors.2 小时前
正则表达式及其常见使用(Kotlin版)
正则表达式
wangkay882 小时前
【Java 转运营】Day05:抖音新号起号:对标账号运营全指南
java·新媒体运营
大飞哥~BigFei2 小时前
新版chrome浏览器安全限制及解决办法
java·前端·chrome·安全·跨域
{Hello World}2 小时前
Java多态:三大条件与实现详解
java·开发语言
老蒋每日coding2 小时前
Java解析Excel并对特定内容做解析成功与否的颜色标记
java·开发语言·excel
lang201509282 小时前
Java反射利器:Apache Commons BeanUtils详解
java·开发语言·apache
m0_748245922 小时前
SQLite 数据类型概述
java·数据库·sqlite
Mh_ithrha2 小时前
题目:小鱼比可爱(java)
java·开发语言·算法