闲话数据库(二)

2016-12-15 朱赟 嘀嗒嘀嗒 嘀嗒嘀嗒


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,就很容易出现这一类的问题。


当然这篇文章其实只提到几个容易踩的坑,并没有深入的去展开讨论每一个问题,原因有三,一是很多问题只有放到实际应用中解释才能容易理解,知道有这些坑,遇到类似问题的时候就可以去网上查。解决的办法也不过是一些固定的套路。二是真的去理解,靠读文章远没有动手去试有效。三是最近真的好累啊,以后有力气再详细写吧。


之前写过的数据库相关的文章: