一、凌晨1:47,手机炸了
我被钉钉的报警声吵醒。
屏幕上赫然写着: 【故障告警】订单系统500错误率飙升 - P0级。
P0,最高优先级,系统不可用。
半小时前,我刚上线了一个“小优化”——改了几行代码,自测没问题,提测没问题。我自信地点了发布,安心睡觉。
现在,凌晨1:47,我看着监控曲线,那条线直直地冲上去,然后断了。
二、那几行代码干了什么
3天前,产品提了个需求:用户输入手机号时,去掉前后的空格。
很简单对吧?我当时的心理活动:5分钟改完,摸鱼半天。
现有代码是这样的:
async function createOrder(req, res) {
const { phone } = req.body;
// 创建订单...
const order = await Order.create({ phone });
res.json({ success: true, order });
}
我加了一行“贴心”的优化:
// 新增:去掉手机号前后的空格
if (phone) {
phone = phone.trim();
}
自测:输入" 13800138000 " → 变成"13800138000" ✅
测试同学也测了几个用例,都过了。
完美,上线!
三、我忽略的“小细节”
问题出在哪?
用户复制手机号时,经常会带一些看不见的字符。比如:
场景一:iPhone用户从通讯录复制
通讯录里的手机号可能是这样的:13800138000
最后面那个不是普通空格,是Unicode的\u00A0(不换行空格)
场景二:从网页复制带国家码的号码
+86 13800138000 中间的短横线或全角空格
场景三:iOS系统自动添加的格式化符号
有些系统会在手机号里悄悄加上 (窄空格)或(软连字符)
我的trim()只去掉了普通空格(\x20),但这些特殊空白字符它根本不认识。
结果是什么?
- 用户输入的手机号:
13800138000\u00A0 - 我的代码:
phone.trim()→ 还是13800138000\u00A0 - 数据库校验:手机号包含非法字符 → 抛错 → 用户下单失败
这些场景占比不高,但刚好集中在凌晨的海外用户和夜猫子用户。监控显示,错误率从0.1%飙升到37%。
四、回滚那5分钟
发现问题后,我面临两个选择:
| 方案 | 耗时 | 风险 |
|---|---|---|
| 热修复 | 15分钟 | 可能引入新问题 |
| 回滚 | 5分钟 | 无损恢复 |
我选了回滚。
1:52,开始操作:
git checkout 上一个稳定版本
npm run build
pm2 restart order-service
每敲一行命令,手都在抖。监控面板上的红线开始回落:
- 1:54 37% → 15%
- 1:55 15% → 3%
- 1:57 3% → 0.2%
- 1:58 恢复正常
1:59,我在群里发:已回滚,系统恢复。
老板回了个:👍
那一刻,这个👍比任何表情都亲切。
五、为什么会犯这个错
1. 对“空白字符”的理解太狭隘
我以为空格就是空格,不知道Unicode里有几十种空白字符:
\u0020普通空格(我认识的)\u00A0不换行空格(我不认识的)\u2000-\u200A` 各种宽度空格(我更不认识的)\u202F窄空格\u205F中数学空格
我的trim()只处理了第一种,剩下的全漏了。
2. 测试用例不完善
我只测了:
- ✅ " 13800138000 " → 普通空格
- ✅ "13800138000" → 正常手机号
我没测:
- ❌ "13800138000\u00A0" → 不换行空格
- ❌ "138 0013 8000" → 中间有空格
- ❌ "+86 13800138000" → 带国家码
3. 缺少防护机制
- 没有代码评审:如果有人问一句“用户复制过来的手机号可能带什么特殊字符”,可能就避坑了
- 没有灰度发布:如果先放量1%,最多影响1%的订单
- 发布后没看监控:错误率其实在上升,但我睡觉了
六、改进方案
事故后,我把代码改成了这样:
async function createOrder(req, res) {
let { phone } = req.body;
if (phone) {
// 1. 去除所有Unicode空白字符
phone = phone.replace(/[\s\u00A0\u2000-\u200B\u202F\u205F]+/g, '');
// 2. 只保留数字和+
phone = phone.replace(/[^\d+]/g, '');
// 3. 如果以86开头但没加号,补上
if (phone.startsWith('86') && !phone.startsWith('+86')) {
phone = '+' + phone;
}
}
// 继续其他逻辑...
}
还写了个专门的测试用例集:
// 测试用例
const testCases = [
' 13800138000 ', // 普通空格
'13800138000\u00A0', // 不换行空格
'138 0013 8000', // 窄空格
'138-0013-8000', // 短横线
'+86 13800138000', // 带国家码和空格
'86138001380000', // 全角数字(另一个坑)
];
还定了几条规则:
- 重要修改必须Code Review,哪怕只改一行
- 先写测试用例再写代码,必须包含边界场景
- 灰度发布,先放量1%观察10分钟
- 不在周五晚上发布,有事白天处理
七、凌晨3点的感悟
那11分钟的事故,让我明白了几件事:
- 你以为的“小优化”,可能是别人的“大灾难”
- 用户的输入永远比你想象的野
- 代码没有“简单”的,每一行都可能出问题
- 凌晨的月亮很圆,但我再也不想看了