前段时间,遇到朋友的求助,说以前运行的好好的系统现在出问题了,而且看不懂报错到底是什么意思。
我仔细看看报错信息,应该是MySQL数据库报出来的,大意是说:collation不兼容,一个是 utf8mb4_0900_ai_ci,另一个是utf8mb4_general_ci。
utf8mb4_general_ci这玩意儿我见过,是针对utf8mb4编码的collation,但是utf8mb4_0900_ai_ci是啥,我也没见过。
于是我问他,这玩意儿从哪里出来的?
他说:“我也不知道,我完全没见过啊。再说,我数据库编码已经是utf8mb4了,怎么还会有这么多名堂?”
看他着急又不知所措的样子,我便花了点时间来研究,还真学到点新知识。而且我也发现,有许多程序员天真的以为“用了UTF8就等于做了国际化了,不用再担心编码问题”。看来,这个话题还真值得多讲讲。
首先从utf8mb4_0900_ai_ci这个诡异的名字说起。
Unicode编码的诞生,是为了解决之前各国的计算机文字编码自成一体的问题。不同国家采用不同的编码,自己用还算正常,但是跨文化交流必然会出问题,更无法解决“在同一篇文档里又要显示中文又要显示韩文还要显示日文”之类的问题。
有了Unicode,地球上所有的文字都有独一无二的编码(Code Point,也就是为它分配的码值,或者说“逻辑代号”),前述问题就解决了。
但是Unicode(有个相关的名字是UCS,Universal Coded Character Set,二者基本等价)只确定了码值,或者说,只分配了逻辑代号。至于这些逻辑代号在实际使用中如何存储,如何传输,那是另一个问题。而UTF-8,就是解决存储和传输等问题的“实际方案”。
实际上,UTF的全名是Unicode Transformation Format,也就是“Unicode变换格式”。这里的“变换”,基本可以类比为:要告诉别人明天早上九点来开会,到底是发邮件呢,还是打电话呢,还是写纸条呢,还是直接去敲门打招呼呢?。
所以,Unicode的变换格式不只UTF-8一种,还有UTF-16、UTF-32等等。UTF-8使用比较普遍,因为它是变长编码,如果只传输ASCII字符,则每个字符只需要一个字节。因此,如果数据中包含大量的ASCII字符,那么UTF-8可以节省很多存储空间。
老一点的程序员大概都知道UTF-8,在MySQL中写作utf8,没有横线。如果要用MySQL存储多种语言的字符,那么把字符集(character set)设定为utf8是合适的选择。注意,MySQL中必须指定utf8,而不是Unicode。因为Unicode只是逻辑规范,utf8才是具体存储和传输的格式。
那么,utf8mb4_0900_ai_ci什么意思呢?
我们分部分来看这个名字,先从开头看起。
utf8mb4,这个名字许多人大概熟悉。如今🖨️🛒♥️☎️🌹之类的emoji表情已经大量使用,但MySQL之前的的字符集(character set)是utf8(更准确的名字是utf8mb3,一个字符最多使用3个字节来存储),只能存储编码值从0x000到0xFFFF之间的字符。
然而,emoji表情字符的码值超过了0xFFFF,按照UTF-8规范,存储时需要用4个字节。正因为如此,MySQL才提供了utf8mb4的字符集。如果把数据库表的字符集设定为utf8mb4,就可以正常存储包含表情字符的文本了。
中间的0900,它对应的是Unicode 9.0的规范。要知道,Unicode规范是在不断更新的,每次更新既包括扩充,也包括修正。比如6.0版新加入了222个中日韩统一表义字符(CJK Unified Ideographs),7.0版加入了俄国货币卢布的符号等等。
如果支持新的Unicode规范,就可以直接享受好处,像对待普通字符那样对待这些新字符,当然是好事。
以前的MySQL虽然也会跟随Unicode的更新,但速度太慢了。MySQL 5.7的第一个发行版MySQL 5.7.1是2013年4月23日面世的,它包含的最新的Unicode规范是Unicode 5.2,发布于2009年10月。即便是2020年1月13日发布的MySQL 5.7.29,仍然是这样。
然而Unicode规范早已升级了很多版,即便是9.0版本,也发布于2016年6月,过去了好多年了。到目前为止,最新的版本已经到了12.1,发布于2019年5月。所以从5.2更新到9.0,看起来是一大进步,其实也只是补课而已。
Unicode在不断更新,来源:维基百科
最后两部分_ai_ci,ai表示accent insensitivity,也就是“不区分音调”,而ci表示case insensitivity,也就是“不区分大小写”。
所以,utf8mb4_0900_ai_ci到底是个什么东西呢?其实,它是个collation。
说起“字符集”,许多人想当然认为,给每个字符分配了一个编码,并且能存储、能传输,这就够了。其实这当然不够,我们不但需要给每个字符分配编码,让它们能存储、能传输,还需要定义一套关系来组织它们,找到它们之间的联系。这套关系的定义,就是collation。
collation定义了哪个字符和哪个字符是“等价”的。所以如果指定“不区分大小写”,那么a和A,e和E就是等价的,这样查找时就会方便很多。但这还不够,世界上的文字很多,所以才会有“不区分音调”的要求,这时候e、ē、é、ě、è就是等价的,那么假设我们要进行拼音查找,只要按e去找就可以全部列出来,很方便。甚至,它们也和ê、ë也是等价的,这样就更方便了。
collation也定义了字符的排序规则,如果按照“字符顺序(而不是简单的‘字母顺序’)”来排序,哪个字符应当排在哪个字符前面。所以,尽管“啊”、“副”、“德”三个字的拼音开头分别为A、F、D,但直接选定collation为utf8mb4,它们并不会按照“啊”、“德”、“副”的顺序排序,而是会排成“副”、“啊”、“德”。如果你希望把中文字符按照拼音(英文字母)来排序,指定使用gb18030_chinese_ci作为collation就可以了。
当然,要补充的是,collation依赖于字符集(character set),所以把gb18030_chinese_ci作为collation,就要求字符集是gb18030,而不能是utf8mb4。
这也很好理解,字符集定义了可以使用的字符,对应的collation定义了字符之间的关系。如果collation不依赖于字符集,那么很可能出现“有些字符没有关系定义,不知如何判断等价和顺序”的问题。
到这里,那位朋友的疑惑就解开了。MySQL 8.0之后,默认collation不再像之前版本一样是utf8mb4_general_ci,而是统一更新成了utf8mb4_0900_ai_ci。
不幸的是,这位朋友的系统是一路升级上来的,所以之前建的各种数据表,它们的collation仍然是utf8mb4_general_ci(这个名字够自负,这个例子也提醒我们,不要低估技术的发展,不要把话说得太满),而新建的表是utf8mb4_0900_ai_ci。如果,恰好遇到包含字符串相等或者大小比较的联表查询语句,而关联的表又使用了不同的collation,MySQL就无法决策到底应当使用哪个,就会报错。
既然如此,解决办法也很简单,用alter table table_name collate utf8mb4_0900_ai_ci显式统一所有表的collation,问题就解决了。
我们可以多想想,把character set和collation分开,到底有什么好处?其实好处很多。如果把字符看作个人,character set就相当于验明正身,给每个字符发张身份证,而collation相当于告诉大家,排队的时候谁在前谁在后。collation有多套,就相当于可以灵活按身高、体重、年龄、出身地等等因素来排序,却完全不会受到身份证号的干扰。
实际上,MySQL 8.0中的collation也确实有多套。从utf8mb4_0900_ai_ci这个名字就可以看出来,起码还有utf8mb4_0900_as_ci,也就是“区分音调”的collation,此时e、ē、é、ě、è就不再是等价的。另外还有utf8mb4_0900_as_cs,此时e和E也不等价了。只要在查询时指定不同的collation,就可以享受这些好处,十分方便。
这个问题本来不麻烦,为什么会难住人呢?原因不复杂,你去看关于MySQL和Unicode的中文资料,绝大部分都是告诉你,utf8或者utf8mb4就可以解决问题了。因此,不少程序员完全意识不到还有collation这种东西。
所以,这些程序员理解的“字符集”就只有一堆孤零零的字符,根本没想到还需要定义字符之间的等价和排序关系。而这恰恰是最可惜的,因为他们完全错过了“举一反三”的启发,许多类似问题也就缺乏解决思路。要知道,哪怕你做的不是国际化的业务,也可以从collation中受益的。
我们都知道,电商系统的订单处理是一个流程,其中涉及许多状态,比如“已下单,未支付”、“已支付”、“已确认”、“已拣货”、“已发货”等等。
有程序员看到这个需求,想当然就按照先后顺序,用1、2、3、4、5来表示对应状态,确实简单不会出错,也方便先后对比,比如要查找所有“已确认”之前的订单,就查查“已确认”的状态码是4,那么找状态码<4的订单就可以。
然后,有一天,忽然要在两个状态之间加入某个中间状态,比如“已确认”之后需要新的风险评估,通过了才可以去拣货,怎么办?总不可能在3和4之间加一个3.5吧?因为这个数据字段本来就是整数型啊。
所以“有经验”一点的程序员会改改,一开始就不按照1、2、3、4、5这样来分配状态码,而是按100、200、300、400、500,留足空隙,这样就避免了3.5的尴尬,直接给“风控系统已通过”分配350就可以了。
但这仍然不够。如果业务忽然要求既有顺序要变,比如之前“已确认”在前,“风控系统已通过”在后,现在要求“风控系统已通过”在前,“已确认”在后,该怎么办?350总不可能大于400呀。
如果你了解了collation就会发现,这是同样的问题。数据的标识和数据的有序性应当隔离开来。标识是一套规范,有序性是另一套规范,两者可以随意组合。你看,Unicode字符的排序可以按照字符的编码值来,也可以按照其它规范来——加载不同collation就是了嘛。
所以,“已下单,未支付”的代码就可以是OUPD,“已支付“的代码就可以是PDED,“已确认”的代码就可以是CFMD…… 它们只用来做唯一标识,没有任何其它意义。然后在外面定义一套顺序规则,比如OUPD < PDED < CFMD,然后提供一个查询接口,做任何比较的时候都查询这个接口就好——实际上许多语言可以自定义compare函数来做排序,道理就在这里。万一将来要改业务流程,比如加入新状态,或者更改状态的先后顺序,也只需要做一点点更改,规则查询接口保持不变,其它地方更是保持原封不动。
最后我想补充的是,即便你有非常多的软件开发经验,但如果要做“国际化”的业务,仍然会面对许多想不到的问题——e、ē、é、ě、è、ê、ë的等价问题就是一例。这类问题,不亲自经历是很难想象的。
From Life Sailor, post MySQL升级8.0的新故障,utf8mb4_0900_ai_ci是啥?
之前我写了一篇《坚持了两年之后,小朋友突然不想去打冰球了…》,本来是无心之作,没想到收到了很多留言,我自己也获益不少。 本来,我以为解决了小朋友的问题,此事就这样过去了。没想到的是,暑假过后,冰球训练重开,他又老调重弹:“我不去了,我不想打冰球了……”。 这可叫我如何是好?听到他嘟嘟囔囔说这一切的时候,我心里百感交集。 成年人的生活里总是有忙不完的事情,对应的,也希望一切井井有条、按部就班。因此,这样“意外”的变数,总是第一时间让人心生无奈和烦恼:天哪,怎么会这样呢?为什么会这样呢? 不过,基于之前的经验,借鉴大家的留言,这次我显然更有心理准备一些,起码不会慌乱。 之前我写过,如果父母多阅读一些高质量的育儿专著,有助于把自己的期望水平“降”到合适的程度,就不会那么焦虑甚至抓狂。 (more…)
认识Michael很偶然,但我也很幸运,因为我见证了一个“打冰球的好孩子”的成长。 最早认识Michael是在冰球队的夏季体能训练上。那时候这群孩子还只有六岁左右,每次训练都是家长送来,在旁边观看陪伴,再接回家。但是,我很快发现有个孩子不一样,家长送他来就回家,他靠自己换好全身装备,训练完自己洗澡更衣,再由家长接回去。看起来,他好像完全没有其他孩子那种“害怕独处”的感觉。 于是我问他:“小朋友,你这么勇敢,你叫什么名字呀?” 他说:Michael。 我尝试复述他的名字,好几遍都不成功,因为我总听成“米歇”,最后他耐着性子慢慢说,我仔细听才发现最后还有个音节,嘴要更扁一点,舌头往上垫,才可以念出来,类似“米歇-厄尔”。其实这个名字写出来大家都认识,英文里读作“迈克尔”,无奈德语的发音规则很严格,字母i不会像英文那样有两种读音,结尾的el又一定要发音,所以就成了“米歇-埃尔”。 (more…)
偶然刷到一篇文章,说的是“贵族家长”群体给小朋友安排的活动:冰球、马术…… 我有点诧异,原来“冰球”也被贴上了“身份”的标签。想想自家小朋友的情况:赶上打折花了400多欧元买的全套护具,80元买的二手冰球包,每个月60欧元的俱乐部费用……想了想,似乎很难和“贵族”联系起来。 只不过,他已经坚持打冰球到了第四年,我们的生活确实有不小的变化。写下来,既是对自己有个交代,也可以作为“贵族运动”的现身说法。因为在我看来,如果非要说它是“贵族”运动,也只能“贵”在高(时间)投入、高产出而已。细细想来,我们的生活,已经被冰球深深的影响了。 (more…)
一 很多人关心,我们父子给M写了道歉信之后,对方是否有回应。 答案是:到目前为止,还没有任何回应。不过比较特殊的是,写完信之后德国小学就开始放秋假,学生不用去学校,既然见不到,也就不可能收到任何回应。 老实说,我觉得对方父母是有点反应过度的。这些年我的一条深刻经验是,如果出现分歧、矛盾,越早、在越低的层面直接面对,就越容易解决。许多小的矛盾之所以越闹越大甚至无法收场,往往都是经过了很多演绎、传话,而没有在一开始就开诚布公地面对。 试想,如果自己的孩子收到写着“我要杀了你”的信件,哪怕一开始很惊慌甚至愤怒,但仔细想一想,毕竟还有很多信息是未知的——比如对方是谁,平时言行如何,为何要写这样的信…… 更好的办法或许是先去直接寻求这些问题的答案,而不是直接把信交给家长委员会,走“公事公办”的路子。 我当然承认,“公事公办”无可厚非,对方家长也有这样的权利——所谓权利,就是“有资格做对方不喜欢的事情,人家还拿你没办法”。既然有这样的权利,就需要尊重。 所以,“严于律己,宽于待人”的确是与人相处的重要法则:我不会选择这么做,但我能理解和尊重你这么做的权利。 也有人问,那将来你遇到M的父母,会不会紧张? 答案是:不会。 (more…)
一 收到S老师邮件的时候,我刚刚胆战心惊地做完第一次德语技术分享,还在享受着同事们的鼓励。猛然间就收到一封邮件:“您的孩子在学校参与了一起性质严重的事件,您必须来学校面谈,请从以下时间段中选择……” 什么?“性质严重的事件”?我揉了揉眼睛,确认自己没有看错。再把这段文字贴到谷歌翻译里,确认自己没有理解错。 我没有看错,也没有理解错,就是“性质严重的事件”。好吧,既然“性质严重”,那谈话肯定是越早越好,最早的日期是第三天。我紧赶慢赶,回信确认了最早可能的谈话时间,虽然德国人通常都不期待能这么快收到回复。 去接他回来的路上,我发现他一切正常,完全看不出任何异样。于是,我也没有表现出任何异样,只是依照惯例,问他当天发生了什么,在学校开心不开心。 得到肯定的答复之后,我心生疑惑,看起来和“性质严重”完全不搭边。那会是什么事情呢? 我又问他,有没有和同学吵架、打架,是不是被人欺负了不敢说。但是,答案全都是“没有”。 我满心怀疑,又按捺不住,直接问:“既然一切都挺好,为什么S老师给我发信,说让我来学校跟她谈话呢?”我担心“性质严重”会吓到他,故意隐去了这个词。 他的满面春风在那瞬间凝固了,喃喃低语道:“好吧,原来是那件事,我还以为她不会跟你说。” (more…)
在2024年之前,我从来没想过自己有一天还可以加入乐团,甚至参加音乐会演奏。我只是个普通中年人,在之前文章里说过,上世纪八十年代随大流弹了十年手风琴,考过六级(当时最高八级)之后就彻底放弃了。直到二十多年后,在上海工作时才重新开始弹琴,当时有幸跟夏老师学了两年,打开了感官,懂得了音乐的世界远远比考级要广阔和美妙。再往后,就是自己看Youtube学习了一些乐理知识。因为德国几乎每个城市都有很多音乐学校,2023年末,我给本市的音乐学校写信,询问是否可以参加手风琴课程。通过回信我才知道,原来不只是“每个城市都有很多音乐学校”,而且“每个城市都有很多乐团”,哪怕是手风琴乐团。就这样,阴差阳错的,2024年初,经过简单的试奏,我加入了本市的手风琴乐团。虽然我是乐团新人,仍然有很多要学习的,但是一年下来,确实有不少感受。如果读者朋友也对音乐感兴趣,或者想让孩子学习音乐,也许我的感受可以提供一些参考。 (more…)
View Comments
感觉用状态码做例子不是很好。在我看来,状态码设计是一个破坏MECE的问题,导致的不协调。
讲的很生动有趣,解决了我的疑惑,顺便还提到了状态码的排序,感谢作者٩(ˊᗜˋ*)و