一、背景
今天上班的时候发生了一件事,客户的服务器 CPU 的使用率持续的出现告警,登录服务器发现达到了 3000% 的 CPU 使用,使用 Arthas 查看线程堆栈发现全部都是 URL 正则匹配导致的问题。感慨一个简单正则匹配算法,因为使用了复杂度比较高的算法,在海量数据的环境下居然会出现这么大的影响。
这是一个非常典型的性能问题 ⚠️------正则表达式匹配 URL 导致 CPU 使用率过高 。
我们可以从根本原因分析 → 优化方向 → 替代方案 三个层次来理解和解决。
二、为什么正则会导致高 CPU 占用?
-
正则表达式本身是"回溯型算法"
- Java(以及大多数语言)默认的正则引擎是 NFA(非确定有限自动机)。
- 在复杂的表达式(特别是带有重复量词
.*
,.+
,(.*)+
,(?:a|aa)*
等)时,会出现指数级回溯。 - 对于长字符串(例如 URL、HTML 文本等),某些匹配会让正则"疯狂回溯",CPU 飙升。
举例:
java
String pattern = ".*(http|https)://.*";
String url = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
// 极易造成大量无意义的匹配尝试
-
URL 结构复杂
- URL 含有多种字符:
/ ? & = # % +
等。 - 如果正则没有精确限定,会触发"贪婪匹配 + 回溯"。
- 特别是在循环或批量日志扫描中使用时,CPU 会飙升。
- URL 含有多种字符:
-
重复编译 Pattern
- 每次
Pattern.compile(regex)
都会重新构建 DFA 树。 - 在高频调用(如每秒数万次匹配)下,这个开销非常大。
- 每次
三、优化方向
1. 预编译 Pattern
错误示例:
java
boolean isMatch = Pattern.matches("https?://[^\\s]+", url);
每次都重新编译正则!
正确做法:
java
private static final Pattern URL_PATTERN = Pattern.compile("https?://[^\\s]+");
boolean isMatch = URL_PATTERN.matcher(url).find();
在高并发系统中,这一项 alone 就能减少 90% 以上 CPU 消耗。
2. 优化正则表达式结构
URL 匹配常见优化:
regex
^https?://[A-Za-z0-9._~:/?#@!$&'()*+,;=%-]+$
要点:
- 限制匹配范围:
^
和$
。 - 避免贪婪的
.*
。 - 用字符类
[A-Za-z0-9...]
替代.*
。
3. 避免在循环中重复创建 Matcher
错误示例:
java
for (String url : urls) {
Matcher matcher = Pattern.compile(REGEX).matcher(url);
if (matcher.find()) ...
}
正确示例:
java
Pattern p = Pattern.compile(REGEX);
for (String url : urls) {
Matcher matcher = p.matcher(url);
if (matcher.find()) ...
}
4. 分层匹配(先粗后细)
如果你只是想判断字符串中是否包含 URL:
java
// 先粗筛
if (url.contains("http")) {
// 再用正则精确匹配
...
}
这种"分层判断"能大幅减少进入正则的次数。
5. 使用高性能替代方案
如果你频繁解析或匹配 URL:
-
使用标准库解析器:
javatry { new java.net.URL(url); } catch (MalformedURLException e) { // not a valid URL }
内部是基于状态机实现的,性能远优于正则。
-
使用 RE2/J(Google 实现的无回溯正则引擎)
xml<dependency> <groupId>com.google.re2j</groupId> <artifactId>re2j</artifactId> <version>1.7</version> </dependency>
javaimport com.google.re2j.Pattern; Pattern p = Pattern.compile("https?://[^\\s]+");
RE2 不会出现"指数回溯",CPU 占用稳定。
四、如何定位正则导致的高 CPU
-
使用 Java Flight Recorder / VisualVM / async-profiler
- 观察 CPU 栈中是否存在
java.util.regex.Pattern$Branch.match
或BacktrackMatcher
. - 如果出现这些方法在栈顶 → 说明是正则回溯问题。
- 观察 CPU 栈中是否存在
-
通过日志确认输入数据特征
- 找出那些引起 CPU 飙升的长字符串。
- 检查正则是否会在这些输入上产生灾难性回溯。
五、推荐方案总结表
场景 | 推荐方案 | 原因 |
---|---|---|
简单 URL 判断 | 预编译轻量正则 | 快速、低成本 |
解析 URL 结构 | java.net.URL |
无回溯,稳健 |
大规模匹配 | RE2/J 引擎 | 防止灾难性回溯 |
批量数据流 | 粗筛 + 精匹配 | 控制匹配频率 |
动态匹配规则 | 缓存 Pattern | 避免重复编译 |