数据库及分布式事务

数据库是软件开发中必不可少的组件,无论是关系型数据库MySQL、Oracle、PostgreSQL,还是NoSQL数据库HBase、MongoDB、Cassandra,都针对不同的应用场景解决不同的问题。本章不会详细介绍这些数据库的使用方法,因为读者或多或少都使用过这些数据库,但是数据库底层的原理尤其是存储引擎、数据库锁和分布式事务是我们容易忽略的,而这些原理对于数据库的调优和疑难问题的解决来说比较重要,因此本章将针对数据库存储引擎、数据库索引、存储过程、数据库锁和分布式事务展开介绍,希望读者能够站在更高的层次理解这些原理,以便在数据库出现性能瓶颈时做出正确的判断。
7.1 数据库的基本概念及原则
7.1.1 存储引擎
数据库的存储引擎是数据库的底层软件组织,数据库管理系统(DBMS)使用存储引擎创建、查询、更新和删除数据。不同的存储引擎提供了不同的存储机制、索引技巧、锁定水平等功能,都有其特定的功能。现在,许多数据库管理系统都支持多种存储引擎,常用的存储引擎主要有MyISAM、InnoDB、Memory、Archive和Federated。1. MyIASM
MyIASM是MySQL默认的存储引擎,不支持数据库事务、行级锁和外键,因此在INSERT(插入)或UPDATE(更新)数据即写操作时需要锁定整个表,效率较低。
MyIASM的特点是执行读取操作的速度快,且占用的内存和存储资源较少。它在设计之初就假设数据被组织成固定长度的记录,并且是按顺序存储的。在查找数据时,MyIASM直接查找文件的OFFSET,定位比InnoDB要快(InnoDB寻址时要先映射到块,再映射到行)。
总体来说,MyIASM的缺点是更新数据慢且不支持事务处理,优点是查询速度快。
2. InnoDB
InnoDB为MySQL提供了事务(Transaction)支持、回滚(Rollback)、崩溃修复能力(Crash Recovery Capabilities)、多版本并发控制(Multi-versioned Concurrency Control)、事务安全(Transaction-safe)的操作。InnoDB的底层存储结构为B+树,B+树的每个节点都对应InnoDB的一个Page,Page大小是固定的,一般被设为16KB。其中,非叶子节点只有键值,叶子节点包含完整的数据,如图7-1所示。
在这里插入图片描述
图7-1
InnoDB适用于有以下需求的场景。
◎ 经常有数据更新的表,适合处理多重并发更新请求。
◎ 支持事务。
◎ 支持灾难恢复(通过bin-log日志等)。
◎ 支持外键约束,只有InnoDB支持外键。
◎ 支持自动增加列属性auto_increment。3. TokuDB
TokuDB的底层存储结构为Fractal Tree。Fractal Tree的结构与B+树有些类似,只是在Fractal Tree中除了每一个指针(key),都需要指向一个child(孩子)节点,child节点带一个Message Buffer,这个Message Buffer是一个先进先出队列,用来缓存更新操作,具体的数据结构如图 7-2所示。这样,每一次插入操作都只需落在某节点的Message Buffer上,就可以马上返回,并不需要搜索到叶子节点。这些缓存的更新操作会在后台异步合并并更新到对应的节点上。
在这里插入图片描述
图7-2
TokuDB在线添加索引,不影响读写操作,有非常高的写入性能,主要适用于要求写入速度快、访问频率不高的数据或历史数据归档。4. Memory
Memory表使用内存空间创建。每个Memory表实际上都对应一个磁盘文件用于持久化。Memory表因为数据是存放在内存中的,因此访问速度非常快,通常使用Hash索引来实现数据索引。Memory表的缺点是一旦服务关闭,表中的数据就会丢失。
Memory还支持散列索引和B树索引。B树索引可以使用部分查询和通配查询,也可以使用不等于和大于等于等操作符方便批量数据访问,散列索引相对于B树索引来说,基于Key的查询效率特别高,但是基于范围的查询效率不是很高。
7.1.2 创建索引的原则
创建索引是我们提高数据库查询数据效率最常用的办法,也是很重要的办法。下面是常见的创建索引的原则。
◎ 选择唯一性索引:唯一性索引一般基于Hash算法实现,可以快速、唯一地定位某条数据。
◎ 为经常需要排序、分组和联合操作的字段建立索引。
◎ 为常作为查询条件的字段建立索引。
◎ 限制索引的数量:索引越多,数据更新表越慢,因为在数据更新时会不断计算和添加索引。
◎ 尽量使用数据量少的索引:如果索引的值很长,则占用的磁盘变大,查询速度会受到影响。
◎ 尽量使用前缀来索引:如果索引字段的值过长,则不但影响索引的大小,而且会降低索引的执行效率,这时需要使用字段的部分前缀来作为索引。
◎ 删除不再使用或者很少使用的索引。
◎ 尽量选择区分度高的列作为索引:区分度表示字段值不重复的比例。
◎ 索引列不能参与计算:带函数的查询不建议参与索引。
◎ 尽量扩展现有索引:联合索引的查询效率比多个独立索引高。

范式是具有最小冗余的表结构,三范式的概念如下所述。1.第一范式
如果每列都是不可再分的最小数据单元(也叫作最小的原子单元),则满足第一范式,第一范式的目标是确保每列的原子性。如图 7-3所示,其中的Address列违背了第一范式列不可再分的原则,要满足第一范式,就需要将Address列拆分为Country列和City列。
图7-3
在这里插入图片描述
2.第二范式
第二范式在第一范式的基础上,规定表中的非主键列不存在对主键的部分依赖,即第二范式要求每个表只描述一件事情。如图 7-4所示,Orders表既包含订单信息,也包含产品信息,需要将其拆分为两个单独的表。
在这里插入图片描述
3.第三范式
第三范式的定义为:满足第一范式和第二范式,并且表中的列不存在对非主键列的传递依赖。如图 7-5所示,除了主键的订单编号,顾客姓名依赖于非主键的顾客编号,因此需要将该列去除。
在这里插入图片描述

7.1.4 数据库事务
数据库事务执行一系列基本操作,这些基本操作组成一个逻辑工作单元一起向数据库提交,要么都执行,要么都不执行。事务是一个不可分割的工作逻辑单元。事务必须具备以下4个属性,简称ACID属性。
◎ 原子性(Atomicity):事务是一个完整操作,参与事务的逻辑单元要么都执行,要么都不执行。
◎ 一致性(Consistency):在事务执行完毕时(无论是正常执行完毕还是异常退出),数据都必须处于一致状态。
◎ 隔离性(Isolation):对数据进行修改的所有并发事务都是彼此隔离的,它不应以任何方式依赖或影响其他事务。
◎ 永久性(Durability):在事务操作完成后,对数据的修改将被持久化到永久性存储中。

7.1.5 存储过程
存储过程指一组用于完成特定功能的SQL语句集,它被存储在数据库中,经过第一次编译后再次调用时不需要被再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象,我们可以基于存储过程快速完成复杂的计算操作。以下为常见的存储过程的优化思路,也是我们编写事务时需要遵守的原则。
◎ 尽量利用一些SQL语句代替一些小循环,例如聚合函数、求平均函数等。
◎ 中间结果被存放于临时表中,并加索引。
◎ 少使用游标(Cursors):SQL 是种集合语言,对于集合运算有较高的性能,而游标是过程运算。比如,对一个 50 万行的数据进行查询时,如果使用游标,则需要对表执行50万次读取请求,将占用大量的数据库资源,影响数据库的性能。
◎ 事务越短越好:SQL Server支持并发操作,如果事务过长或者隔离级别过高,则都会造成并发操作的阻塞、死锁,导致查询速度极慢、CPU占用率高等。
◎ 使用try-catch处理异常。
◎ 尽量不要将查找语句放在循环中,防止出现过度消耗系统资源的情况。

7.1.6 触发器
触发器是一段能自动执行的程序,和普通存储过程的区别是“触发器在对某一个表或者数据进行操作时触发”,例如进行UPDATE、INSERT、DELETE操作时,系统会自动调用和执行该表对应的触发器。触发器一般用于数据变化后需要执行一系列操作的情况,比如对系统核心数据的修改需要通过触发器来存储操作日志的信息等。

7.2 数据库的并发操作和锁

7.2.1 数据库的并发策略 数据库的并发控制一般采用三种方法实现,分别是乐观锁、悲观锁及时间戳。1. 乐观锁 乐观锁在读数据时,认为别人不会去写其所读的数据;悲观锁就刚好相反,觉得自己读数据时,别人可能刚好在写自己刚读的数据,态度比较保守;时间戳在操作数据时不加锁,而是通过时间戳来控制并发出现的问题。2. 悲观锁 悲观锁指在其修改某条数据时,不允许别人读取该数据,直到自己的整个事务都提交并释放锁,其他用户才能访问该数据。悲观锁又可分为排它锁(写锁)和共享锁(读锁)。3. 时间戳 时间戳指在数据库表中额外加一个时间戳列TimeStamp。每次读数据时,都把时间戳也读出来,在更新数据时把时间戳加 1,在提交之前跟数据库的该字段比较一次,如果比数据库的值大,就允许保存,否则不允许保存。这种处理方法虽然不使用数据库系统提供的锁机制,但是可以大大提高数据库处理的并发量。