小Cの已经记不起来的博客

正则表达式进阶:分布式锁 匹配技巧

最近线上有个 Redis 分布式锁老是误释放,想先把日志里的 lock_acquirelock_releaselock_failed 这些事件抓出来,顺便把 keyownerttlreentrant 提取出来做告警。结果一上手就发现日志格式从 0.16 开始多了一个 namespace 前缀,原来那条 lock.* 的破正则全废了。然后就开始翻文档和旧脚本,发现官方其实只给了一点样例,真正能用的写法还得自己拼。为什么不能直接给一条成熟正则呢?可能是我瞎没找到吧...

下面是实测可用的匹配方式,需要借助一个能跑 python 的环境,不然你就只能把字符串贴到正则测试网站上一点点试,效率很低。

先把日志格式钉死

我这边日志大概长这样:

event=lock_acquire key=lock:order:123:550e8400-e29b-41d4-a716-446655440000 owner=worker-1 ttl=30s reentrant=1
event=lock_release key=lock:order:123:550e8400-e29b-41d4-a716-446655440000 owner=worker-1 ttl=30s
event=lock_failed key=lock:order:999:6fa459ea-ee8a-3ca4-894e-db77e160355e owner=worker-2 ttl=15s

如果你直接写 key=(\S+) owner=(\w+),看起来能用,但它会把 ttl=30s 也当成 owner 后面的内容,更坑的是当某一行没有 ttl 或者多了 namespace=order,分组就开始错位。正则这种玩意儿,宁可一开始写严格一点,也别等线上误报再改。

先写主体,再处理可选项

import re

TEXT = '''
event=lock_acquire key=lock:order:123:550e8400-e29b-41d4-a716-446655440000 owner=worker-1 ttl=30s reentrant=1
event=lock_release key=lock:order:123:550e8400-e29b-41d4-a716-446655440000 owner=worker-1 ttl=30s
event=lock_failed key=lock:order:999:6fa459ea-ee8a-3ca4-894e-db77e160355e owner=worker-2 ttl=15s
'''

LOCK_RE = re.compile(r'''
    (?P<event>lock_acquire|lock_release|lock_failed)
    \s+key=
    (?P<lock_key>
        lock:
        [^:\s]+:
        [^:\s]+:
        [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}
    )
    \s+owner=(?P<owner>[0-9A-Za-z_.-]+)
    (?:\s+ttl=(?P<ttl>\d+)s)?
    (?:\s+reentrant=(?P<reentrant>\d+))?
''', re.X)

for m in LOCK_RE.finditer(TEXT):
    print(m.group('event'), '|', m.group('lock_key'), '|', m.group('owner'), '|', m.group('ttl'), '|', m.group('reentrant'))

这里用了 re.X,可以换行和加空格,不然一条长正则真的没法维护。(?:...) 是非捕获组,意思是括号里的内容只负责组合,不参与最终分组编号,这个在可选项特别好用,比如 ttl 不一定每行都有,但你又不想让它产生一个空分组。

不要乱用 .*

我刚开始写的是这样:

r'key=(.*) owner=(\w+) ttl=(\d+)s'

这种写法在单行测试没问题,一旦同一行里出现两个 ttl=,比如:

event=lock_acquire key=lock:order:123:uuid owner=worker-1 ttl=10s comment=retry ttl=20s

它会直接从第一个 key= 吃到最后一个 ttl=,提取出来的东西完全不能用。更稳的办法是明确禁止匹配分隔符,比如 key 里的 UUID 部分只匹配十六进制和短横线,owner 用字符集,不要用“能匹配一切”的写法。

如果非要用懒惰匹配,至少写成:

r'key=(\S+) owner=(\w+)'

但这个还是不严谨,因为 \S+ 会吃掉 owner=worker-1,因为等号后面也没有空格。真正适合日志键值对的是限制字符集:

r'key=(lock:[^:\s]+:[^:\s]+:[0-9a-fA-F-]+) owner=([0-9A-Za-z_.-]+)'

分布式锁 key 常见是冒号拼接,那就别在字符集里放过冒号以外的未知内容,除非你确定业务 key 可以很野。

用 lookahead 做“必须有”和“不能有”

有些场景不是要提取,而是要过滤。比如只匹配“成功获取锁,并且带 reentrant 计数”的行,但又不想把 reentrant 放进捕获结果。可以这样:

r'event=lock_acquire\s+key=lock:[^\s]+\s+owner=[^\s]+(?=\s+reentrant=\d+)'

(?!...) 也一样常用,比如排除 lock:debug: 这种测试 key:

r'key=(lock:?!debug:)[^\s]+'

不过实际维护时我不太喜欢这种太隐晦的写法,排查起来会骂人。如果只是提取字段,还是老老实实写顺序更可靠。

多行日志和命名空间前缀

后来发现新版本会把日志改成 JSON:

{"ts":"2026-09-12T10:00:01Z","level":"info","msg":"lock","event":"lock_acquire","key":"lock:order:123:550e8400-e29b-41d4-a716-446655440000","owner":"worker-1","ttl":30}

这种时候就别硬拿正则一行一行啃了,能用 jq 就用 jq,能用结构化解析就用结构化解析。正则不是不能用,只是没必要。比如快速过滤还是可以用:

grep '"event":"lock_release"' logs/redis-lock.log

要提取 key 和 owner:

grep '"event":"lock_release"' logs/redis-lock.log \
  | grep -oP '"key":"[^"]+","owner":"[^"]+"'

但如果后面要做统计,还是建议用 Python 解析 JSON 后再输出 CSV,不然正则和 JSON 转义会互相折磨。

对应需要替换的 Python 脚本配置部分

import json
import re
import sys

PATTERN = re.compile(r'"event":"(?P<event>lock_acquire|lock_release|lock_failed)"')

for line in sys.stdin:
    try:
        data = json.loads(line)
    except json.JSONDecodeError:
        continue

    event = data.get('event')
    if event not in {'lock_acquire', 'lock_release', 'lock_failed'}:
        continue

    key = data.get('key', '')
    owner = data.get('owner', '')
    ttl = data.get('ttl', '')
    namespace = data.get('namespace', '')

    if not key.startswith('lock:'):
        continue

    print('\t'.join([event, namespace, key, owner, str(ttl)]))

跑起来:

python lock_match.py < logs/redis-lock.log > lock_events.tsv

不出问题的话就没有问题了,终端里能看到几列数据,然后再丢给 sort | uniq -c 看哪个 key 释放次数最多,哪个 owner 抢锁最频繁。最后只需要把这个脚本放到你的日志清洗任务里,或者先临时在跳板机上跑一遍验证一下即可。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放