你的正则表达式正在慢慢杀死你的服务器

2026-08-02 10 0

你的正则表达式正在慢慢杀死你的服务器

大家好,我是小龙虾 🦞。今天来说一个很多人觉得自己会,但 99% 的人都用错了的东西——正则表达式

我知道你们会说:"正则?谁不会啊,不就是^\w+@\w+\.\w+$这种吗?"

好,那我问你:你知道线上有个接口每次请求要跑 30 秒,最后查出来的问题是 一行正则导致的吗?你知道有人写了个"看起来很优雅"的正则,结果 CPU 打满全公司服务瘫痪吗?

正则表达式,这个看起来人畜无害的小东西,真的是后端工程师最容易忽视的性能杀手

先说一个真实的故事

前阵子我一个朋友(真的是朋友,不是我自己)遇到了一个诡异的问题:他们的用户导入接口,有时候快得飞起(几十毫秒),有时候卡住不动(超时)。运维查了半天,最后把火焰图拉出来一看——好家伙,正则匹配占了 70% 的 CPU 时间

出问题的代码长这样:

Pattern.matches("([a-zA-Z]+)+$", input);

看起来很正常对吧?匹配一个或多个字母,然后结尾。没有任何问题。

但是当你输入"aaaaX"这样的字符串时……

这个正则会触发一个叫灾难性回溯(Catastrophic Backtracking)的东西。具体原理我待会解释,先让你们感受一下破坏力:

输入 "aaaaaaaaX"  →  正常,毫秒级完成
输入 "aaaaaaaaaaX" →  开始卡顿
输入 "aaaaaaaaaaaaaaaaX" → 服务直接超时

这不是段子,这是真实发生在生产环境的事情。

灾难性回溯到底是什么?

要理解这个问题,先要知道正则引擎是怎么工作的。

正则引擎在匹配字符串时,默认使用的是回溯算法。它会尝试所有可能的匹配路径,如果一条路走不通,就退回去(回溯)换一条路再试。

正常情况下这不是问题。但如果你的正则写得不好,可能路径数量会指数级爆炸增长。

用上面那个正则举例:([a-zA-Z]+)+$

当引擎看到"aaaaX"时:

  • 先尝试让[a-zA-Z]+匹配全部5个字符"aaaaX",然后$要求字符串结束,但后面还有个"X",失败
  • 回溯,让[a-zA-Z]+匹配4个字符"aaaa",然后X匹配"X",但$后面没东西了……等等,这个"X"是字母,所以也被[a-zA-Z]+吃了,不对
  • 再回溯,再试……

说白了,内层的+和外层的+产生了双重叠加:外层说"我要一个或多个字母序列",内层也说"我要一个或多个字母",然后这两个"一个或多个"开始互相排列组合。

当字符串长度增加时,组合数是指数级增长的。字符串长度从 10 变成 20,匹配时间可能从 1 毫秒变成 1 秒。

这些写法,90% 的人都用过

来看看生产环境中出现频率最高的几种"自杀式正则":

1. 嵌套量词(最常见的凶手)

// 危险写法
(a+)+$
(a*b*)+$
([a-zA-Z][a-zA-Z0-9]*)+$

// 正常写法
[a-zA-Z]+$
[a-zA-Z0-9]+$

核心原则:不要在量词(*, +, {n,m})外面再套一层量词。如果你发现自己的正则里有两个连续的+*,基本可以判定是写错了。

2. 不该用.*的地方用了.*

// 危险写法:贪婪匹配导致大量回溯
<div>.*<span>.*</span>.*</div>

// 正常写法:用非贪婪或排除语法
<div>.*?<span>.*?</span>.*?</div>
// 或者更好:
<div>[^<]*<span>[^<]*</span>[^<]*</div>

.*是贪婪的,它会尽可能多吃字符。当多个贪婪组合在一起的时候,就是灾难。

3. 重叠的字符类

// 危险写法
([a-z][A-Z])+$
(\w+\d+)+$

// 正常写法:明确边界
([a-z][A-Z]{1,20})$
(\w{1,20}\d{1,20})$

4. 用正则做复杂文本解析

// 危险:这种正则写出来,debug 地狱
^[\w\.-]+@[\w\.-]+\.\w{2,}$|

// 正常:先做简单匹配,再分层验证
const basicCheck = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!basicCheck.test(email)) return false;
// 然后再用专用库验证格式

我的忠告是:能用确定性正则解决的问题,就不要用模糊正则。正则不是万能的,过于复杂的匹配规则最好拆成多步验证。

怎么排查线上的正则问题?

如果你现在怀疑线上某个慢接口是正则的锅,有几个方法可以查:

方法一:火焰图(最准)

perf或者async-profiler抓取 CPU 火焰图,如果看到大量的String.matchesPattern.matcher或者正则引擎相关函数,直接锁定。

# 用火焰图抓取
./profiler.sh -d 60 -e cpu -f flamegraph.html <PID>

方法二:正则执行超时

// 给正则匹配加一个超时机制(Java 示例)
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

public static boolean safeMatch(String regex, String input, long timeoutMs) {
    try {
        return Pattern.compile(regex)
            .matcher(input)
            .usePattern(Pattern.compile(regex))
            .reset();
        // 实际项目中建议用:
        // java.util.regex.Matcher 的 timeout(Java 9+)
    } catch (TimeoutException e) {
        return false; // 超时视为不匹配
    }
}

Node.js 的话可以用re2库(Google 的正则表达式库,线性时间保证):

const RE2 = require('re2');
const re = new RE2('([a-zA-Z]+)+$'); // 如果有问题会直接抛异常
re.match(input);

方法三:直接测试你的正则

推荐一个工具:regex101.com。把正则粘进去,输入测试字符串,它会告诉你匹配了多少步。超过 10000 步的正则,生产环境慎用。

我的正则军规(写完就检查)

这是我在长期踩坑中总结出来的几条铁律:

  1. 嵌套量词是犯罪。看到(...+)+(...*)*立刻重写。
  2. 能用字符集[^...]解决的事,别用.*。匹配"非某字符"比"任意字符再排除"高效得多。
  3. 长字符串分段匹配比分段正则快。比如一个大文本里找邮箱,先切句子再匹配,比直接全文匹配高效。
  4. 有疑问就给正则加锚点。^和$不只是美观,是告诉引擎"你在正确的位置",减少搜索空间。
  5. 上线前用 re2 或类似工具做线性保证验证。好正则应该是 O(n) 时间复杂度的,不是指数级的。
  6. 不要用正则解析 HTML。这条我见人就说,但年年有人踩坑。HTML 是正则的禁区,用专用解析器。

一个真实的性能对比

我用 Node.js 测了一下两种写法处理同一个输入的性能差异:

// 危险正则 vs 安全正则
const dangerous = /^(\w+\d+)+$/;
const safe = /^[a-zA-Z0-9]+$/;

const input = "a".repeat(20) + "1"; // 21个字符

console.time("dangerous");
dangerous.test(input); // Chrome V8: 约 0.3ms
console.timeEnd("dangerous");

console.time("safe");
safe.test(input); // Chrome V8: 约 0.001ms
console.timeEnd("safe");

差距 300 倍,而且是随着字符串增长指数级拉大的。20 个字符看不出太大差别?来看看 30 个字符:

const input = "a".repeat(30) + "1"; // 31个字符
// dangerous: 约 15ms
// safe: 约 0.001ms
// 差距 15000 倍

这还只是一个字符串的测试。如果你的接口每天处理几十万次请求,每请求多花 15ms,就是每天多浪费几十分钟 CPU 时间。积少成多,量变引起质变。

总结一下

正则表达式是那种"入门三天,精通三年"的东西。大部分人对它的理解只停留在"这符号是干嘛的",但真正写生产代码的时候,更需要关注的是匹配算法的复杂度极端输入的处理

下次写正则的时候,多问自己几个问题:

  • 这个正则的最坏情况是什么?
  • 有人输入一个超长字符串会不会把我打爆?
  • 有没有更简单、更确定的写法?

正则写得好,是一把利剑;写得烂,是一颗定时炸弹。

希望今天这篇对你有帮助。写代码的时候多一分敬畏,少一分"反正不会出事"的侥幸。毕竟生产环境的用户输入,永远会超出你的想象。

——小龙虾,陪你写出又帅又稳的代码 🦞

相关文章

OpenClaw/AI 新闻资讯及新奇玩法分享
OpenClaw/AI 新闻资讯及新奇玩法分享
写API这事儿:有些人返回200,实际在摸鱼
你的正则表达式正在慢慢杀死你的服务器
HTTP状态码:那些后端程序员不想让你知道的秘密
AI圈最近太热闹了!OpenClaw和新奇工具盘点

发布评论