数据库三种范式以下。算法
范式 | 描述 | 反例 |
---|---|---|
第一范式 | 每一个字段都是原子的,不能再分解 | 某个字段是JSON串,或者是数组 |
第二范式 | 1) 表必须有主键,能够是多个顺序属性的组合。2) 非主属性必须彻底依赖主属性(这里指的是组合主键),而不能部分依赖。 | 好友关系表中,主键是关注人ID和被关注人ID,表中存储的姓名等字段只依赖主键中的一个属性,不彻底依赖主键 |
第三范式 | 没有传递依赖(非主属性必须直接依赖主属性,不能间接依赖主属性) | 在员工表中,有部门ID和部门名称等,部门名称直接依赖于部门ID,而不是员工ID |
通常工程中,对于数据库的设计要求达到第三范式,但这不是必定要遵照的,因此在开发中,为了性能或便于开发,出现了不少违背范式的设计。如冗余字段、字段中存一个JSON串,分库分表以后数据多维度冗余存储、宽表等。数据库
关于B树和B+树的概念,请见漫画算法:什么是 B+ 树?数组
下面来看一下数据库中主键索引对应的B+树的逻辑结构。并发
该结构有几个关键特征(与通常的B+树有点不一样)异步
基于B+树的这几个特征,就能够很容易的实现范围查询、前缀匹配模糊查询、排序和分页(查询条件应是索引)。这里有几个要注意的问题。性能
select *** where *** limit 100, 10
,数据库须要遍历前面100条数据才知道offset=100的位置在哪。合理的分页方法就是不使用offset,把offset变成条件来查询。好比变成select *** where *** and id > 100 limit 10
,这样才能利用索引的遍历,快速定位id=100的位置。事务的四大特性ACID。设计
事务与事务并发地操做数据库的表记录,可能会致使下面几类问题。日志
问题 | 描述 |
---|---|
脏读 | 一个事务A读取了另外一个未提交的事务B的数据,可是事务A提交以前,事务B又回滚了,致使事务A刚刚读到的就是一个脏数据(RC隔离级别可解决)。 |
不可重复读 | 同一个事务两次查询同一行记录,获得的结果不同。由于另外一个事务对该行记录进行了修改操做(行排它锁可解决)。 |
幻读 | 同一个事务两次查询某一范围,获得的记录数不同,由于另外一个事务在这个范围内进行了增长或删除操做(临键锁可解决)。 |
丢失更新 | 两个事务同时修改同一行记录,事务A的修改被后面的事务B覆盖了(须要本身加锁来解决)。 |
下面来看一下InnoDB的事务隔离级别。可解决上面的三个问题,最后一个问题只能在业务代码中解决。code
名称 | 描述 |
---|---|
READ_UNCOMMITTED(RU) | 跟没有同样,几乎不使用。 |
READ_COMMITTED(RC) | 只能读取另外一个事务已提交的事务,能防止脏读。 |
REPEATABLE_READ(RR) | 可重复读(在一个事务内屡次查询的结果相同,其它事务不可修改该查询条件范围内的数据,会根据条件上Gap 锁) |
SERIALIZABLE | 全部的事务依次逐个执行,至关于串行化了,效率过低,通常也不使用。 |
关于数据库的锁,请详见这篇。MySQL中的“锁”事cdn
为了保证数据的持久性,须要每提交一个事务就刷一次磁盘,可是这样效率过低了,因此就有了Write-Ahead。
Write-Ahead:先在内存中提交事务,而后写日志(在InnoDB中就是redo log,日志是为了防止宕机致使内存数据丢失),而后再后台任务中把内存中的数据异步刷到磁盘。
从逻辑上来说,日志就是一个无限延长的字节流,从数据库启动开始,日志就一直在增长,直到数据库关闭。
在逻辑上,日志是按照时间顺序从小到大用LSN(是一个64位的数)来编号的,由于事务有大有小,因此日志是个变长记录(每一段数据量都不同)。
从物理上来说,日志不多是一个无限延长的字节流,由于每一个文件有大小限制。在物理上是整块的读取和写入(这里就是Redo Log 块,一个块就是512字节),而不是按字节流来处理的。并且日志是能够被覆写的,由于当数据被刷到磁盘上后,这些日志也就没有用了,因此他们是能够被覆盖的。可循环使用,一个固定大小的文件,每512字节一个块。
在InnoDB中,Redo Log采用先以Page为单位记录日志(物理记法),每一个Page里再采用逻辑记法(记录Page里的哪一行修改了)。这种记法就叫作Physiological Logging。
之因此采用这种记法,是逻辑日志和物理日志的对应关系决定的。
由于不一样事务的日志在Redo Log中是交叉存在的,因此无法把未提交的事务与已提交的事务分开。ARIES算法的作法就是,无论事务有没有提交,它的日志都会被记录到Redo Log上并刷到磁盘中。当崩溃恢复的时候,会把Redo Log所有重放一遍(不论是提交的仍是未提交都都重作了,也就是彻底恢复到崩溃以前的状态),而后再把未提交的事务给找出来,作回滚处理。
其实事务的回滚都是反向提交。也就是根据事务中的SQL语句生成反向对应的SQL语句执行,而后Commit(这种逆向的SQL语句也会被记录到Redo Log中,防止恢复中宕机,可是会与正常的日志区分开),因此回滚是逻辑层面上的回滚,在物理层面实际上是个提交。
以下图,有六个事务,每一个事务所在的线段表示事务在Redo Log中的起始位置和结束位置。发生宕机时,须要回滚事务T三、T四、T5。
在图中,绿线表示两个Checkpoint点和Crash(宕机)点。蓝线表示三个阶段工做的起始位置。
在分析阶段,要解决两个问题。
1)肯定哪些数据页是脏页,为阶段2的Redo作准备(找出从最近的Checkpoint到Crash之间全部未刷盘的Page)。
2)肯定哪些事务未提交,未阶段3的Undo作准备(由于未提交的事务也写进了Redo Log中,须要将这些事务找出来,并作回滚)。
ARIES的Checkpoint机制,通常使用的是Fuzzy Checkpoint,它在内存中维护了两个表,活跃事务表和脏页表。
1)活跃事务表:当前全部未提交事务的集合,每一个事务维护了一个关键变量lastLSN(该事务产生的日志中最后一条日志的LSN)。 2)脏页表:当前全部未刷到磁盘上得Page的集合(包括未提交事务和已提交事务),recoveryLSN是致使该页为脏页的最先LSN(最近一次刷盘后最先开始的事务产生的日志的LSN)。
每次Fuzzy Checkpoint,就是把这两个表的数据生成一个快照,造成一条checkponit日志,记入Redo Log中。
下图展现了事务的开始标志(S表示Start transaction),结束标志(C表示Commit)以及Checkpoint在Redo Log中的位置(这里只展现了活跃事务表,脏页表也是相似的,惟一不一样的就是脏页集合只会增长,不会减小,脏页集合中可能有些页是干净的,但因为Redo Log是幂等的,因此不影响)。
阶段1中已经准备好了脏页集合,取集合中脏页的recoveryLSN的最小值(也就是最先开始脏的那一页),获得firstLSN,在Redo Log中从firstLSN开始遍历到末尾,把每条Redo Log对应的Page所有从新刷到磁盘中。可是这些脏页中可能有些页并非脏的,因此这里要作幂等。也就是利用Page中的一个PageLSN字段(它记录了当前Page刷盘时最后一次修改它的日志对应的LSN),在Redo重放的时候,判断若是日志的LSN比磁盘中得PageLSN要小,那就直接略过(这点很是相似TCP的超时重发中的判重机制)。在Redo完成后,保证了全部的脏页都刷到了磁盘中,而且未提交事务也写入了磁盘中,这时须要对未提交事务进行回滚,也就是阶段3。
阶段1中已经准备好了未提交事务集合,从最后一条日志逆向遍历(每条日志都有一个preLSN字段,指向前一条日志),直到未提交事务中的第一条日志。
从后往前开始回滚,每遇到一条属于未提交事务集合中事务的日志,就生成一条对应的逆向SQL(这里须要用到对应的历史版本数据)执行,这条逆向SQL也会被记录到Redo Log中,但与通常的日志有所不一样,称为Compensation Log Record(CLR),逆向执行完事务后(遇到事务的开始标志)就提交,这也就完成了回滚。
上面在宕机回滚中,提到了生成逆向SQL,这个是须要使用到历史版本数据的。Undo Log就是用于记录和维护历史版本数据的(事务的每一次修改,就是一个版本)。其实这是用到了CopyOnWrite的思想,每次事务在修改记录以前,都会把该记录拷贝一份出来(将它备份在Undo Log中)再进行修改操做(这个思想相似JDK中CopyOnWriteArratList)。事务的RC、RR隔离级别就是经过CopyOnWrite实现的。
并发的事务,要同时读写同一行数据,只能读取数据的历史版本,而不能读取当前正在被修改的数据(因此这样就有了丢失更新的问题,固然这个能够经过加锁等方式解决)。这种机制称为Multiversion concurrency control 多版本并发控制(MVCC)。基于MVCC的这种特性,一般select语句都是不加锁的,由于他们读到的都是历史版本的数据,这种读,叫作“快照读”。
Undo Log并非log,而是数据(因此Undo Log也会被记录到Redo Log中,在宕机后用Redo Log来恢复Undo Log),它记录的不是事务执行的日志,而是数据的历史版本。一旦事务提交后,就不须要Undo Log了(它只在事务提交过程当中有用)。
因此Undo Log应该叫作记录的备份数据,也就是在事务提交以前的备份数据(由于可能有其它事务还在引用历史版本数据),事务提交后它就没有用了。
Undo Log的结构除了主键ID和数据外,还有两个字段。一个是修改该记录的事务ID,一个是rollback_ptr(指向以前的一个版本,因此它用来串联全部的历史版本)。