数据库原理

事务

事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。

ACID

1. 原子性(Atomicity)

事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。

回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。

2. 一致性(Consistency)

数据库总是从一个一致性状态转换到另外一个一执行的状态

数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。

3. 隔离性(Isolation)

一个事务所做的修改在最终提交以前,对其它事务是不可见的。

4. 持久性(Durability)

一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。

使用重做日志来保证持久性

事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:

  • 只有满足一致性,事务的执行结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
  • 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对数据库崩溃的情况。

隔离级别

READ UNCOMMITED (未提交读)

在 READ UNCOMMITED 级别,事务中的修改,即使没有提交,对其他事物也都是可见的。事务可以读取未提交的数据,这也被称为脏读(Dirty Read)。

READ COMMITED (提交读)

大多数数据库系统的默认隔离级别都是 READ COMMITED (MySQL不是)。READ COMMITED 满足隔离性的简单定义:一个事务所做的修改在最终提交以前,对其它事务是不可见的。这种级别有时候也叫不可重复读(nonrepeatable read)。

REPEATABLE READ (可重复读)

REPEATABLE READ级别保证了在同一个事务中多次读取同样记录的结果是一致的,但是REPEATABLE READ无法解决幻读的问题。

幻读:指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。

REPEATABLE READ是MySQL默认的事务隔离级别

SERIALIZABLE (可串行化)

SERIALIZABLE是最高的隔离级别,它通过强势事务串行执行,避免了幻读问题

当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。所以,锁主要用于处理并发问题。

从数据库系统角度分为三种:排他锁、共享锁、更新锁。
从程序员角度分为两种:一种是悲观锁,一种乐观锁。

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。

传统的关系数据库里用到了很多这种锁机制,比如按使用性质划分的读锁、写锁和按作用范围划分的行锁、表锁。

读锁

读锁又称共享锁(S锁),若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

写锁

写锁又称拍他锁(X锁),若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。

封锁粒度

MySQL 中提供了两种封锁粒度:行级锁以及表级锁。

应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。

但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。

在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。

表锁

每次操作锁住整张表,开销小,加锁快,锁粒度大,发生锁冲突的概率最高,并发度最低。

行级锁

每次操作锁住一行数据,开销大,加锁慢,锁粒度小,发生锁冲突的概率最低,并发度最高。

数据库能够确定哪些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

版本号控制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

ABA问题

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时就会误以为它的值没有发生变化,这个问题称为ABA问题。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A就会变成1A-2B-3A,以此来防止不恰当的写入。
JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

封锁协议

在运用X锁和S锁对数据对象进行加锁时,还需要约定一些规则,例如何时申请X锁或S锁、持锁时间、何时释放等,这些规则称为封锁协议,对封锁方式规定不同的规则,就形成了各种不同的封锁协议。

一级封锁协议

一级封锁协议即事务在修改某行数据时必须先对其加X锁,直到事务结束才释放,事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。一级封锁协议可以防止丢失修改,因为此时别的事务要想修改该数据将会阻塞到对方释放X锁,但如果仅仅是读数据不对其进行修改,是不需要加锁的,也就不能保证可重复读和脏读问题。

二级封锁协议

二级封锁协议即在一级封锁协议的基础上,事务读取某数据之前必须先对其加S锁,读完后即可释放S锁。二级封锁协议除了防止丢失修改,还可以进一步防止脏读问题,但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。

三级封锁协议

三级封锁协议即在一级封锁协议的基础上,事务读取某数据之前必须先对其加S锁,直到事务结束才释放。三级封锁协议除了防止丢失修改和脏读问题以外,还进一步防止了不可重复读。

两段锁协议

一次性锁协议指的是在事务开始时,一次性申请所有的锁,之后不会再申请任何锁,如果其中某个锁不可用,则整个申请就不成功,事务就不会执行,一次性释放所有的锁。一次性锁协议不会产生死锁的问题,但事务的并发度不高。

数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁):

  • 加锁阶段:事务只能加锁,也可以操作数据,但不能解锁,直到事务释放第一个锁,就进入解锁阶段
  • 解锁阶段:事务只能解锁,也可以操作数据,但不能加锁

两段锁协议使得事务具有较高的并发度,但是没有解决死锁的问题,因为它在加锁阶段没有顺序要求,如两个事务分别申请了A、B锁,接着又申请了对方的锁,此时进入死锁状态。

数据库规范化

函数依赖

部分函数依赖

设X、Y是关系R的两个属性集合,存在X→Y,若X’是X的真子集,存在X’→Y,则称Y部分函数依赖于X。

完全函数依赖

设X、Y是关系R的两个属性集合,X’是X的真子集,存在X→Y,但对每一个X’都有X’ !→Y,则称Y完全函数依赖于X。

传递函数依赖

设X、Y、Z是关系R中互不相同的属性集合,存在X→Y(Y !→X),Y→Z,则称Z传递函数依赖于X。

范式

第一范式(1NF)

在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。

所谓第一范式(1NF)是指数据库表的每一列(每个属性)都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。简而言之,第一范式就是无重复的列。

第二范式(2NF)

第二范式(2NF)要求实体的属性完全依赖于主关键字。

第三范式(3NF)

在满足第二范式的基础上,且不存在传递函数依赖,那么就是第三范式。简而言之,第三范式就是属性不依赖于其它非主属性。

参考资料