数据库建库(最难调试修复的 bug 是怎样的?)

数据库建库(最难调试修复的 bug 是怎样的?)
最难调试修复的 bug 是怎样的?

有一类bug,打断点它就消失,去掉断点它就回来。

不是在跟你开玩笑,这玩意有个正式名字叫Heisenbug——海森堡bug,跟量子力学那个”观测改变结果”的海森堡不确定性原理是一个意思。你观察它,它就不存在。你不观察它,它就出来搞事。

数据库建库(最难调试修复的 bug 是怎样的?)

举个具体的。一段多线程代码偶尔segment fault。开debug模式跑,不崩了。切回release模式,又崩了。反复切了十几次,debug模式下就是稳如老狗。

原因:debug模式关闭了编译器优化,变量不会被优化到寄存器里,内存布局和时序都跟release不一样。bug只在特定内存布局下触发,debug模式恰好绕过了那个布局。

你的调试手段本身就是解药。你拿掉药,病就回来了。

这种bug能让人怀疑人生。你开始在工位上喃喃自语:”不可能啊,逻辑没问题啊。”旁边的同事以为你失恋了。

NULL:什么都没做就把数据搞没了

SQL里有个东西叫NULL。它不是0,不是空字符串,不是false。它是”不知道”。

“不知道”跟任何东西运算的结果都是”不知道”:

SELECT NULL = NULL;    -- 结果是NULL,不是trueSELECT NULL <> 1;      -- 结果是NULL,不是trueSELECT NULL + 100;     -- 结果是NULL

NULL = NULL 的结果不是 true,是 NULL。在SQL的世界观里,”不知道”等不等于”不知道”?不知道。

这个逻辑在哲学上很自洽。在写业务代码的时候能把人逼疯。

-- 查所有不是VIP的用户SELECT * FROM users WHERE level <> 'VIP';

你觉得这能查到level为NULL的用户吗?查不到。因为 NULL <> 'VIP' 的结果是NULL,不是true,WHERE只保留结果为true的行。level没填的用户直接消失了。

得这么写:WHERE level <> 'VIP' OR level IS NULL。

还有更隐蔽的。COUNT(*) 和 COUNT(column) 的区别——COUNT(*) 计算所有行,COUNT(column) 会跳过NULL值。如果你统计”有多少用户填了手机号”,用 COUNT(phone) 是对的。但如果你统计”总共有多少用户”用了 COUNT(phone),那些没填手机号的用户就被你”数没了”。

报表数字死活对不上,查了半天发现是COUNT写错了。这种bug不会报错,不会崩溃,数据看着就是”少了一些”,但你不知道少在哪。

编码问题:mojibake和那个该死的BOM

数据库里存的好好的中文,页面上显示成”锟斤拷”。

这三个字已经成了程序员的创伤后应激触发词了。

原理是这样的:UTF-8编码的文本被当成GBK来解析,无法识别的字节被替换成Unicode的替换字符U+FFFD(就是那个黑色菱形里面一个问号),然后这个替换字符又被编码成UTF-8的三个字节 EF BF BD,两个连在一起 EF BF BD EF BF BD 再被当成GBK解析——刚好就是”锟斤拷”。

修起来倒是不难,把编码统一就行。难的是找到是哪一层搞错了编码。

从浏览器到Web服务器到应用层到数据库驱动到数据库本身,每一层都有自己的编码设置。MySQL连接字符串里有 characterEncoding,数据库建库有 CHARACTER SET,表有表的字符集,列有列的字符集。任何一层不一致就会出问题。

还有一个幽灵杀手:BOM(Byte Order Mark)。

UTF-8文件开头可能有三个不可见字节 EF BB BF。Windows的记事本特别喜欢加这个。文件用文本编辑器打开看着完全正常,但解析的时候这三个字节会被当成内容的一部分。

JSON解析失败、CSV导入报错、XML声明无法识别、Shell脚本第一行的 #!/bin/bash 前面多了三个不可见字符导致”找不到解释器”——全是BOM干的。用 cat -v 一看,文件开头多了个 M-oM-;M-?。用肉眼看文件内容,完全没问题。

浮点数:0.1 + 0.2 ≠ 0.3

>>> 0.1 + 0.20.30000000000000004>>> 0.1 + 0.2 == 0.3False

学过编程的都知道这个。但知道归知道,该踩还是会踩。

做电商的,商品价格用 double 存。单价4.1元,买3个,应该是12.3元。实际算出来12.299999999999999。用户看到订单金额12.30,对账的时候发现总金额对不上。订单量一大,差额就很明显了。财务找过来了。

修了价格,用 BigDecimal。过了两个月,促销模块又炸了——满减计算还是用的 double。一年后,积分兑换模块也炸了——汇率转换还是用的 double。

浮点数的坑不难理解,难的是在一个几十万行的项目里确保每个涉及金额的地方都没用 double。你以为你改完了,半年后某个犄角旮旯的工具类里又冒出来一个。

更阴间的是跨语言调用。Java端用 BigDecimal 算好了,传给前端。JavaScript收到之后自动转成 Number——又变成IEEE 754浮点数了。12.30 变成 12.3,尾部的零没了,发票格式校验失败。

缓存不一致:数据库改了,页面没变

用户改了头像,刷新页面,还是旧头像。再刷,还是。清了浏览器缓存,还是。换个浏览器,新头像出来了。

回到原来的浏览器,还是旧头像。

用户很崩溃,开发也很崩溃。

查了一圈:数据库里是新头像的URL,CDN上新图片也传上去了。问题出在CDN缓存——CDN节点上还缓存着旧的图片URL对应的响应。CDN的缓存TTL设的是24小时。

改成URL加版本号 avatar.jpg?v=2 可以解决。但已经被缓存的那些请求怎么办?等24小时?用户等不了。主动刷新CDN缓存?几十个CDN节点,刷新是异步的,不同用户访问到不同节点,有人看到新的有人看到旧的。

这还是单层缓存。加上Redis缓存、本地缓存(Caffeine/Guava)、Nginx缓存、浏览器缓存,一共四五层。任何一层没更新都会看到旧数据。

有个说法是”计算机科学里只有两个难题:缓存失效和命名”。这话归到Phil Karlton名下,但具体出处已经不可考了。不管谁说的,做过缓存的人都会点头。

差一个:off-by-one

循环少跑了一次,数组多访问了一格,分页漏了最后一条,时间范围查询少了一天。

这种bug最恶心的地方在于——它几乎只在边界条件下触发。数据有100条的时候没问题,数据刚好是分页大小整数倍的时候才出问题。日期范围查一个月没问题,查2月份的时候少了一天(28号还是29号取决于闰年)。

-- 查2月份的数据SELECT * FROM orders WHERE create_time >= '2024-02-01'   AND create_time < '2024-03-01';-- 某人写的SELECT * FROM orders WHERE create_time >= '2024-02-01'   AND create_time <= '2024-02-28';-- 2024是闰年,2月有29号。29号的数据没了。

off-by-one不难修,难的是发现。因为99%的情况下它是对的,只有边界那1%才出错。而测试数据很少刚好卡在边界上。

有人把这个归纳成:”计算机科学里有两个难题:缓存失效、命名、还有off-by-one错误。”

嗯,这个笑话本身就是一个off-by-one。

DATABSE_URL少了个A

环境变量名字写错了一个字母。程序没报错——它读到的是空值,然后fallback到了默认的本地数据库连接。开发环境和测试环境都有本地数据库,所以一路绿灯。到了生产环境,连上了一个空的本地数据库,所有查询返回空结果。

不报错,不崩溃,就是没数据。

CSS的z-index冲突、SQL里把 column_name 写成了 coIumn_name(l和I长得一模一样)、YAML缩进用了tab而不是空格、JSON最后一个元素多了个逗号——你盯着代码看一百遍都看不出来,但换个人一眼就找到了。

查了三天的bug,最后发现是一个拼写错误。你从怀疑代码到怀疑框架到怀疑操作系统到怀疑编译器,最后怀疑自己。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

相关阅读

最新文章

热门文章

本栏目文章