一、关于数据库,最常见的问题有哪些?

1. 数据库的选型

因为每个公司的业务不同,数据库系统的应用场景也不一样,选型也会不尽相同,但可以肯定的是,没有哪个系统一定是最好的,只有相对公司业务场景更适用的

比如:

  • 做支付业务一定要强调事务性、一致性的支持,许多社交平台平时需要的是高可用;
  • 有的业务写操作特别重,有的业务更重要的是读操作;
  • 有的业务可能只关心最近几天的数据,于是可以容忍老数据读写的低效,有的却要频频访问老数据,需要读写的高效;
  • 有的业务可以通过加索引解决查询效率,有的却只能通过加缓存等等。

这就是为什么很多公司会选择多个数据库系统并存,通过不同的技术和架构给予相关业务场景最优支持。如果一旦选型失误,便不会有频频踩坑一说,因为这基本就算直接掉进了坑里。


2. 数据库相关的架构

这里的数据库架构包括数据库上层的缓存系统设计,程序语言对数据库连接的处理,代理层(Proxy Layer)的功能,以及和二进制日志(Binlog)等相关数据管道(Data Pipeline)的搭建。当然,其中也包括了数据库系统的分区、备份等具体设计。

很多公司早期所有的表都在一个数据库里,因为各种连接池(Connection Pool)和吞吐量(Throughput)的限制,基本没法做扩展。能够合理的设计数据库表的分离,把数据相关的放在一起,不那么相关甚至不相关的放在另一个数据库里。这些看起来很简单的做法,很大程度上可以缓解可扩展的问题。


3. 数据库平时遇到最多的错误:人为错误

再好的系统,使用姿势不对也是枉然,更何况并不是所有的工程师都是数据库专家,所以人为的错误是最常出现的问题。

人为错误分为两种:

  1. 数据库操作时犯下的错误;
    操作数据库时犯错的概率比较低,但危害最大。

    常见的数据库操作时犯错的方式有:

    • 工程师无意或者有意,“不小心”删掉了数据库核心表中的所有数据;
    • 工程师在线修改表结构(online schema change)的时候,不小心一步误操作,结果导致数据库被锁长达几个小时,导致相关业务挂掉几个小时;
    • 两台服务器做主从切换时,拔错一个电源插头导致失败。
  2. 开发工程师在程序里或脚本里犯下的错误。
    如果一个程序或者脚本查询没有索引的数据,导致全表扫描,再加上一些网页服务器(Web Server)的并行访问,经常会有整个数据库所有连接被占用的情况,连接终止查询(Kill Query)都没法执行,只能人肉的去做一些物理重启的操作。面对这类问题,可以使用数据库连接查询功能,比如left outer join来避免N + 1的问题。


4. 数据库访问瓶颈

只要是数据库,就会有吞吐量的限制,而数据库访问瓶颈便是自然流量增长或者流量突增造成的。只要你的业务在增长,总有一天数据库访问就会达到一个上限。在这个预警到来前,你需要做各种垂直或水平扩展来提升这个上限,或者,你可以通过缓存和其它机制来对访问量进行分流,这里面可以做的工作就很多了。

流量的突增一般是类似分布式拒接服务(DDoS)或市场活动带来的,也可能是因为某个黑天鹅事件造成的,这些原因都很难预料。

如果是有计划的市场活动,就需要提前做好各种战斗准备。如果是恶意攻击,也就只能依靠各种防御工程,如IP阻塞(IP Blocking)或者第三方高防系统挡掉这些流量来保证数据库的正常工作。


二、研发过程中与数据库相关的问题

在这里我们以MySQL为例。

1. 索引

创建索引通常是为了提高查询语句的性能,将某些列以特定的数据结构(常见的如B-Tree)有序存储起来。维持这样的一个数据结构在写数据时会有一些性能开销(Overhead)。但如果查询语句确实是高频的,那么这样的系统开销就很划算。在建表时需要考虑所有可能的高频查询,另一方面,要忌讳过度的“为未来设计”(Desgin for the future),也就是加一堆根本不常用的索引,反而增加了写数据时的成本和负担。

索引另一个常见的用途就是保证某一列或者某几列的组合是唯一的(Unique),这也称为唯一性索引(Unique Indexing),在写业务逻辑代码时会常常用到。

比如你有一个用户表(User Table),你想让所有用户(User)的电子邮件都是唯一的,这个时候用唯一索引(Unique Indexing)就很方便。不过唯一索引(Unique Indexing)和可选列(Optional Column)组合在一起的时候,也有很多需要注意的地方。

比如:你想对X做唯一索引(Unique Indexing),过了一段时间,也有有些情况下X(Column X)并不唯一,我们便把索引改成了对列X和列Y做唯一索引(Unique Index Column X + Column Y),但是列Y(Column Y)时Nullable的,那么这个时候会出现什么情况呢————你会有很多记录,有着一样的X值,以及Null的Y值(因为Null在数据库里常常解释为“不确定”而不是空)。


2. 事务支持

数据库的事务支持(Transactional Support),简单来说,就是利用数据库本身的事务性,来封装一系列需要同时完成的动作。

比如:在一段事务里面,先执行X,再执行Y(Transaction do X;Y;end),如果X和Y都是数据库写操作,那么两个写操作要么都成功,要么都失败。也就是说,对数据库的改动会统一把事务所做的修改提交(Commit)到数据库,而提交(Commit)前的任何错误都会触发所有更新回滚到开始的状态(Rollback)或引发不正常的进程终止(Abort)。

虽然正确的使用事务支持(Transactional Support)会很方便,但是也常常见到过度使用让代码变得很脆弱甚至出现BUG的情况。

常见的情况如下:

  • 事务(Transaction)中封装的代码逻辑太长太复杂,甚至调用了别的函数。很多时候,很难去推理当执行中抛出异常的话,到底哪些会回滚,哪些会产生遗留影响。

  • 事务中封装了与数据库改动无关的逻辑。

  • 事务中存在不可逆的操作,如发送电子邮件给用户,发布(Publish)到一个Job队列(Queue)等。这种情况会导致系统的不一致。比如:一个写操作被回滚了,但这条数据相关的Job还是被加入到队列了,就会引发错误。

  • 事务中包含了在不同数据库里的事务,也就是分布式事务,这需要单独处理。

  • 事务中嵌套了事务,不同情况可能会有不同结果,如果没搞清楚,就可能会出现意外的行为。

    过过度使用事务支持往往会让逻辑变得不必要的复杂。


3. 数据库锁

数据库会出现Race Condition,我们常常把Race Condition叫做竞争条件,是指多个进程或者线程并发访问和操作同一数据,且执行结果与访问发生的特定顺序有关的现象。

如何解决竞争条件(Race Condtion)呢?常见的方法是使用各种锁机制来确保行为的可预测性和正确性。根据实际情况的不同,加锁的方式会不一样。

常见的有乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)。总的来说,前者在对性能要求比较高的系统里比较常见。在实际应用中,很多系统会自己实现锁定(Locking)机制。


4. 缓存和主从机制

为了提高性能,我们会为数据库增加缓存(Cache)和主从(Master-Slave)等机制,这时候会引起数据的不一致性。常见的情况是,如果系统默认实在从(Slave)节点读取数据,那么一些刚刚更新到主节(Master)的数据在读的时候就有可能读不到。这个情况在使用一些数据关联(Association)的时候更容易读不到。Rails的Active Record数据关联(Association),就容易出现这一类问题。