今天为大家送上一篇很有意思的小文章,具有提神醒脑之功效。作者是来自阿里巴巴LAZADA产品技术部的申徒童鞋。
1. 血案由来
近期我在为Lazada卖家中心做一个自助注册的项目,其中的shop name校验规则较为复杂,要求:
1. 英文字母大小写
2. 数字
3. 越南文
4. 一些特殊字符,如“&”,“-”,“_”等
看到这个要求的时候,自然而然地想到了正则表达式。于是就有了下面的表达式(写的比较龊):
^([A-Za-z0-9._()&'\- ]|[aAàÀảẢãÃáÁạẠăĂằẰẳẲẵẴắẮặẶâÂầẦẩẨẫẪấẤậẬbBcCdDđĐeEèÈẻẺẽẼéÉẹẸêÊềỀểỂễỄếẾệỆfFgGhHiIìÌỉỈĩĨíÍịỊjJkKlLmMnNoOòÒỏỎõÕóÓọỌôÔồỒổỔỗỖốỐộỘơƠờỜởỞỡỠớỚợỢpPqQrRsStTuUùÙủỦũŨúÚụỤưƯừỪửỬữỮứỨựỰvVwWxXyYỳỲỷỶỹỸýÝỵỴzZ])+$
在测试环境,这个表达式从功能上符合业务方的要求,就被发布到了马来西亚的线上环境。结果上线之后,发现线上机器时有发生CPU飙到100%的情况,导致整个站点响应异常缓慢。通过dump线程trace,才发现线程全部卡在了这个正则表达式的校验上:
一开始难以置信,一个正则表达式的匹配过程怎么可能引发CPU飚高呢?抱着怀疑的态度去查了资料才发现小小的正则表达式里面竟然大有文章,平时写起来都是浅尝辄止,只要能够满足功能需求,就认为达到目的了,完全忽略了它可能带来的性能隐患。
引发这次血案的就是所谓的正则“回溯陷阱(Catastrophic Backtracking)”。下面详细介绍下这个问题,以避免重蹈覆辙。
2. 正则表达式引擎
说起回溯陷阱,要先从正则表达式的引擎说起。正则引擎主要可以分为基本不同的两大类:一种是DFA(确定型有穷自动机),另一种是NFA(不确定型有穷自动机)。简单来讲,NFA 对应的是正则表达式主导的匹配,而 DFA 对应的是文本主导的匹配。
DFA从匹配文本入手,从左到右,每个字符不会匹配两次,它的时间复杂度是多项式的,所以通常情况下,它的速度更快,但支持的特性很少,不支持捕获组、各种引用等等;而NFA则是从正则表达式入手,不断读入字符,尝试是否匹配当前正则,不匹配则吐出字符重新尝试,通常它的速度比较慢,最优时间复杂度为多项式的,最差情况为指数级的。但NFA支持更多的特性,因而绝大多数编程场景下(包括java,js),我们面对的是NFA。以下面的表达式和文本为例,
text = ‘after tonight’
regex = ‘to(nite|nighta|night)’
在NFA匹配时候,是根据正则表达式来匹配文本的,从t开始匹配a,失败,继续,直到文本里面的第一个t,接着比较o和e,失败,正则回退到 t,继续,直到文本里面的第二个t,然后 o和文本里面的o也匹配,继续,正则表达式后面有三个可选条件,依次匹配,第一个失败,接着二、三,直到匹配。
而在DFA匹配时候,采用的是用文本来匹配正则表达式的方式,从a开始匹配t,直到第一个t跟正则的t匹配,但e跟o匹配失败,继续,直到文本里面的第二个 t 匹配正则的t,接着o与o匹配,n的时候发现正则里面有三个可选匹配,开始并行匹配,直到文本中的g使得第一个可选条件不匹配,继续,直到最后匹配。
可以看到,DFA匹配过程中文本中的字符每一个只比较了一次,没有吐出的操作,应该是快于NFA的。另外,不管正则表达式怎么写,对于DFA而言,文本的匹配过程是一致的,都是对文本的字符依次从左到右进行匹配,所以,DFA在匹配过程中是跟正则表达式无关的,而 NFA 对于不同但效果相同的正则表达式,匹配过程是完全不同的。
3. 回溯