from:bibliolectors.tumblr.com
上周有几个朋友从国内过来硅谷旅游。用 “旅游” 这个词好像不那么合适,但是若说 “参观学习” 又似乎无趣了些。不过你别说,这一行有一位,入海关的时候,跟海关工作人员说是要来 “硅谷旅游”,结果差点没让进来。理由是:旅游为什么要来硅谷啊?老美觉得这很奇怪的样子。想想看,硅谷这地方,天天呆在这,觉得特别舒服,不过若真说什么好玩的,倒也没觉得。不过上周陪着朋友去一些地方走走,发现有些地方还是挺值得一看的。
虽然硅谷也算人口密集度很高的地区,可是和国内大城市比起来,公共交通并不发达,因此没有车,便有点寸步难行的感觉。所以我也有幸给几位充当了几次司机。说来女司机一直是个梗,不过我其实天天 Carpool,每周开车也算不少,所以其实还算是很靠谱的那一类。但是可能这边车相对比较少,起步加速也快,开得也快,所以几位朋友一路哇哇叫着说我车开的太猛了。甚至坐在后排的上车时并没有系安全带,车刚开出不久的时候,听见后排咔咔的系安全带的声音,也是醉了。
途中提及几位朋友,说起有一些做 DBA 的后来想转研发。心里默想着,若是一个工程师真的很懂 DB,在工作中是有极大的优势的。很多数据库相关的基础知识,若是真的不知道,工作里遇到的麻烦可能根本无从下手,或是出了错也不能理解为什么。因此说这些是每个工程师都应该懂的也不为过。然而我试过在面试中和一些 Candidate 讨论一些数据库相关的话题,意外地发现,答得不好的也大有人在。所以这样看来并不是所有人都把它当作是必须技能。
最近因为数据库相关的东西碰的比较多,就再写写一些日常工作中常常遇到的问题或事情景吧。还是以最常见的 MySQL 为例吧。其他数据库虽然情况不太一样,但很多道理是相通的。
首先就是创建 Table 时候的 Indexing。Indexing 是为了提高一些常用查询模式或语句的性能,而将某些列以特定的数据结构(常见的如 B-Tree)有序存储起来。维持这样的一个数据结构在写数据的时候会有一些 Overhead,但是如果其加快的查询确实是高频的,那么这样的 Overhead 就很划算。所以一方面,在建表时需要考虑所有可能的高频查询,另一方面,忌讳过度地 “Design for the Future”,也就是加了一堆可能根本不常用的 Index,反而增加了写数据时候的 Cost。
Indexing 另一个常见的用途就是保证某一列或者某几列的组合是 Unique 的,这也称为 Unique Indexing。这在写业务逻辑的代码时有时候很有用。比如你有一个 User table,想让所有 User 的 Email 都是 Unique 的,用 Unique Indexing 就很方便。不过 Unique Indexing 和 Optional Column 组合在一起的时候,也有很多需要注意的地方。打个比方,你想 Unique index column X,过了一段时间,也许有些情况下 Column X 并不惟一,所以把 Index 改成 Unique index Column X + Column Y,但是 Column Y 是 nullable 的。这个时候会出现什么情况呢?你可以有多个 Records,有着一样的 X value,以及 null 的 Y value。很意外对吧,原因就是 null 在数据库里常常解释为 “不确定” 而不是空。
另一个特别常用,也特别容易犯错的问题就是 DB 的 Transactional Support。简单说来,就是利用数据库本身提供的事务性支持,来 wrap 一段需要同时完成的动作。比如 Transaction do X;Y;end,如果 X 和 Y 都是数据库写操作,那么或者两者的写都会成功,或者两者的写都会失败。换句话说,对数据库的改动会统一 commit,commit 前的任何错误都会触发所有更新的 rollback 或 abort。然而 Transactional Support 虽然在正确被使用的时候会很方便,但是也常常见到过度使用让代码变得很脆弱甚至是 buggy 的情况。常见的几种情况:
Transaction 中 wrap 的代码逻辑太长太复杂,甚至调用了别的函数。很多时候,很难去推理当执行中 raise Exception 的话,到底哪些会 rollback,哪些会产生遗留影响。
Transaction 中 wrap 数据库改动无关的逻辑。
Transaction 中有不可逆的操作,例如发送 email 给用户,publish 到一个 job Queue 等。这种情况会引发系统的不一致。比如,一个被 rollback 的change,destroy 了一个数据,但是这个数据相关的 job 还是被 enqueue 到队列了,就会引发错误。
Transaction 中包括了在不同 DB 里面的事务。
Transaction 中嵌套了 Transaction,不同情况可能会有不同的结果,如果没有搞清楚,就可能会有意外的行为。
还有别的情况就不一一列举了,但是过度使用 Transactional Support 往往会让逻辑变得不必要的复杂。
还有就是数据库相关的 Race Condition。常见的方法是使用各种锁机制来确保行为的可预测性和正确性。根据实际情况的不同,加锁的方式会不一样。常见的有 Optimistic Locking 和 Pessimistic Locking。总的说来,前者在对性能要求比较高的系统里更常见。很多系统里都是自己实现 Locking 机制。
数据库使用中另一个常见的问题,就是一些为了提高性能增加的 Caching 和 Slave 等机制,有时候能引起数据的不一致性。常见的情况,如果系统默认是从 Slave 读数据,那么一些刚刚更新的 Record 在读的时候就有可能读不到。这个情况在使用一些数据 Association 的时候更容易读不到。Rails 的 ActiveRecord 中的 Association,就很容易出现这一类的问题。
当然这篇文章其实只提到几个容易踩的坑,并没有深入的去展开讨论每一个问题,原因有三,一是很多问题只有放到实际应用中解释才能容易理解,知道有这些坑,遇到类似问题的时候就可以去网上查。解决的办法也不过是一些固定的套路。二是真的去理解,靠读文章远没有动手去试有效。三是最近真的好累啊,以后有力气再详细写吧。
之前写过的数据库相关的文章:
或是邮件反馈可也:
askdama[AT]googlegroups.com
订阅 substack 体验古早写作:
关注公众号, 持续获得相关各种嗯哼: