跟前端一起来学数据库系列(3)

上两篇分享我们讲了如何设计数据库表和基本的SQL语句知识,有了这些知识我们就可以愉快的使用数据库来做一两个Demo程序了。但光有这些知识还是不够的,生产环境的情况远不是Demo环境能比的,你可能会遇到数据库查询耗费时间太长导致机器满载而无法提供可靠的服务,还可能会遇到断电或者别并发量太大导致的数据不一致问题等等,这些在生产环境里出现了都会严重影响主业务,所以本次分享就来简单的聊一聊如何解决这些问题。

在讲主要内容之前,先插播一些数据库数据存储方面的知识,我们现在用的数据库大部分都是mysql,所使用的主要引擎是InnoDB,所以我们接下讲的东西都是基于InnoDB的。

那么数据在InnoDB引擎的数据库里是怎么存储的呢? 数据库底层使用了一种名为B+树的数据结构,B+树是为磁盘或其他直接存取辅助设备而设计的一种平衡查找树(如果不知道平衡查找树,请自行google),在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶节点中,各叶节点指针进行连接。 B+树的数据结构类似下面这样:
此处输入图片的描述 所以数据在数据库中的存储类似下面图中显示的这样: 此处输入图片的描述


有时候为了提高查询的效率我们会使用索引,那么索引是什么呢?
索引是对数据库表中一个或多个列(例如,employee表的姓名 (name) 列)的值进行排序的结构。例如这样一个查询:select * from table1 where id=10000。如果没有索引,必须遍历整个表,直到ID等于10000的这一行被找到为止;有了索引之后(必须是在ID这一列上建立的索引),即可在索引中查找。由于索引是经过某种算法优化过的,因而查找次数要少的多。可见,索引是用来定位的。

索引分为以下几种:

  • 普通索引:最基本的索引类型,没有唯一性之类的限制
  • 唯一索引:唯一索引是不允许其中任何两行具有相同索引值的索引
  • 主键索引:在数据库关系图中为表定义主键将自动创建主键索引,主键索引是唯一索引的特定类型。该索引要求主键中的每个值都唯一。当在查询中使用主键索引时,它还允许对数据的快速访问
  • 候选索引:与主索引一样要求字段值的唯一性,并决定了处理记录的顺序。在数据库和自由表中,可以为每个表建立多个候选索引
  • 聚集索引:也称为聚簇索引,在聚集索引中,表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引
  • 非聚集索引:也叫非簇索引,在非聚集索引中,数据库表中记录的物理顺序与索引顺序可以不相同。一个表中只能有一个聚集索引,但表中的每一列都可以有自己的非聚集索引

其中聚集索引的查找步骤如下图所示: 此处输入图片的描述

非聚集索引和聚集索引一样,都是采用平衡树作为索引的数据结构。索引树结构中各节点的值来自于表中的索引字段,如果给表中多个字段加上索引,那么就会出现多个独立的索引结构,每个索引(非聚集索引)互相之间不存在关联。结构如下图所示: 此处输入图片的描述

非聚集索引和聚集索引的区别在于,通过聚集索引可以查到需要查找的数据,而通过非聚集索引可以查到记录对应的主键值,再使用主键的值通过聚集索引查找到需要的数据,如下图所示: 此处输入图片的描述

不管怎么查表到最后都会通过主键使用聚集索引来查到最终的记录, 但有一种称为覆盖索引的方法可以不通过这种方式来查询记录。覆盖索引要求要查的数据刚好位于索引中,比如,用name和age建立了一个联合索引,这时候获取全部的age大于18的name,这时候就不用通过聚集索引获取相应的记录了,因为所需要的数据索引上就有,如下图所示: 此处输入图片的描述

从上面可以看到使用了索引之后,查询的效率将会得到很大的提升,那为什么不默认给所有的列都加上索引呢?索引并不是越多越好,索引也有缺点,主要有下面这几点:

  • 每次给字段建一个新索引,字段中的数据就会被复制一份出来,用于生成索引。因此,给表添加索引,会增加表的体积, 占用磁盘存储空间。
  • 索引能让数据库查询数据的速度上升,而使写入数据的速度下降,原因很简单,因为平衡树这个结构必须一直维持在一个正确的状态, 增删改数据都会改变平衡树各节点中的索引数据内容,破坏树结构,因此,在每次数据改变时,DBMS必须去重新梳理树(索引)的结构以确保它的正确,这会带来不小的性能开销
  • 对重复数据太多的字段加索引效果不理想

讲完了索引,接下来我们谈谈事务。
数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。

事务具有以下特点(ACID):

  • Atomicity(原子性):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
  • Consistency(一致性):数据库总是从一个一致性状态转换到另一个一致状态。
  • Isolation(隔离性):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。
  • Durability(持久性):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。(持久性的安全性与刷新日志级别也存在一定关系,不同的级别对应不同的数据安全级别。)

其中,事务的隔离性是通过锁、MVCC等实现,事务的原子性、一致性和持久性则是通过事务日志实现。

当有大量并发请求访问数据库时,可能会造成以下问题:

  • 丢失更新:撤销一个事务时,把其他事务已提交的更新数据覆盖(A和B事务并发执行,A事务执行更新后,提交;B事务在A事务更新后,B事务结束前也做了对该行数据的更新操作,然后回滚,则两次更新操作都丢失了)。
  • 脏读:一个事务读到另一个事务未提交的更新数据(A和B事务并发执行,B事务执行更新后,A事务查询B事务没有提交的数据,B事务回滚,则A事务得到的数据不是数据库中的真实数据。也就是脏数据,即和数据库中不一致的数据)。
  • 不可重复读:一个事务读到另一个事务已提交的更新数据(A和B事务并发执行,A事务查询数据,然后B事务更新该数据,A再次查询该数据时,发现该数据变化了)。
  • 覆盖更新:这是不可重复读中的特例,一个事务覆盖另一个事务已提交的更新数据(即A事务更新数据,然后B事务更新该数据,A事务查询发现自己更新的数据变了)
  • 虚读(幻读):一个事务读到另一个事务已提交的新插入的数据(A和B事务并发执行,A事务查询数据,B事务插入或者删除数据,A事务再次查询发现结果集中有以前没有的数据或者以前有的数据消失了)。

为了解决这些问题,数据库定义了下面几种隔离级别:

  • SERIALIZABLE(序列化):添加范围锁(比如表锁,页锁等),直到transactionA结束。以此阻止其它transactionB对此范围内的insert,update等操作。幻读,脏读,不可重复读等问题都不会发生。
  • REPEATABLE READ(可重复读):对于读出的记录,添加共享锁直到transactionA结束。其它transactionB对这个记录的试图修改会一直等待直到transactionA结束。可能发生的问题:当执行一个范围查询时,可能会发生幻读。
  • READ COMMITTED(提交读):在transactionA中读取数据时对记录添加共享锁,但读取结束立即释放。其它transactionB对这个记录的试图修改会一直等待直到A中的读取过程结束,而不需要整个transaction A的结束。所以,在transactionA的不同阶段对同一记录的读取结果可能是不同的。可能发生的问题:不可重复读。
  • READ UNCOMMITTED(未提交读):不添加共享锁。所以其它transaction B可以在transactionA对记录的读取过程中修改同一记录,可能会导致A读取的数据是一个被破坏的或者说不完整不正确的数据。另外,在transactionA中可以读取到transactionB(未提交)中修改的数据。比如transactionB对R记录修改了,但未提交。此时,在transactionA中读取R记录,读出的是被B修改过的数据。可能发生的问题:脏读。 下图总结了以上几种情况: 此处输入图片的描述

后记:
三篇分享终于写完了,其实这几次分享本来是用Keynote来做的,主要是以说为主,基本都是图,但分享完后工头说要写个文字版,奈何文笔太差,一直迟迟不想写,最后在煎熬中还是写完。写完之后自己看了一遍,果然还是无法直视,就这样吧,如果文章能让你了解一些东西我还是很高兴的,当然没有也没关系,毕竟我的水平也很有限。
以上。