本文翻译自Coding-Geek文章:《 How does a relational database work》。
原文链接:http://coding-geek.com/how-databases-work/#Buffer-Replacement_strategies
紧接上一篇文章,本文翻译了例如以下章节:
Transaction manager(事务管理器)
一、关于ACID
一个满足ACID标准的事务,符合下面四个条件:
- Atomicity(原子性):
一个事务要么完整的运行全部对数据库的操作,要么不正确数据库做不论什么操作,即使要持续运行10个小时。假设事务中止了。数据库将返回到事务运行前的状态(事务回滚)。
- Isolation(隔离性): A和B两个事务同一时候运行。不管哪个事务先运行完都不影响终于的运行结果。
- Durability(持久化):一旦事务成功提交;数据将持久化、保存到数据库,不管兴许发生何种异常。
Consistency(一致性): 仅仅有满足数据库约束的有效数据能写到数据库。
一致性与原子性和隔离性强相关。
在同一个事务中,你能够运行多条SQL语句去查询、改动、新增、删除数据库中的数据。当多个事务同一时候訪问一份数据时,混乱出现了。最经典的样例就是转账汇款,从A账号转账到B账户。
想象一下,有这样两个事务:
- 事务1:从A账户转账100美元到B账户。
- 事务2:从A账户账户50美元到B账户。
相应到事务的ACID原则上来说:
- Atomicity(原子性):
确保发生不论什么故障(server崩溃、网络中断等)都不会出现100美元从A账户扣除了。却没有存入B账户的情况(数据不一致)。 - Isolation(隔离性):
事务1和2同一时候运行。终于结果始终是A账户降低150美元。B账号添加了150美元。不会出现A账号降低了150美元,B账户仅仅添加了50美元的情况(事务2的运行结果覆盖的事务1,出现了数据不一致)。 - Durability(持久化):假设事务1成功提交,事务的运行结果将被保存到数据库。数据不会凭空丢失,即使数据库发生问题。
- Consistency(一致性): 确保在转账的过程中,总金额是一致的,A账户降低多少钱,B账户就相应的添加多少钱。
现代数据库不会使用纯粹、全然的事务隔离,由于它会带来极大的性能损耗。SQL规范定义了四种隔离模式。
- Serializable(串行化运行,
SQLite默认级别):最高隔离性级别。同一时候运行的两个事务全然隔离,每一个事务有独立的运行空间。 Repeatable Read(可重读,
MySQL默认级别):每一个事务独立运行。只是,假设一个事务加入了新数据。并已提交完成。另外一个正在运行的事务能看到新加的数据。但,假设一个事务是改动数据后提交完成,还有一个正在运行的事务是看不到这样的改动的。,在新添加数据的情况下。破环的事务了的隔离性。比如:事务A正在运行“SELECT COUNT(*) FROM
TABLE_X”。这时事务B往TABLE_X加入了数据。假设事务A再次运行COUNT(*)操作。前后两次的查询结果不同。这样的情况被称之为”幻读”。
Read Committed(Oracle、PostgreSQL、SQL
Server默认支持的级别):这样的隔离度是在Repeatable
Read的基础上。添加了一条打破事务隔离性的规则。假设事务A读取了数据D,同一时候事务B对数据做了改动(包含删除)后提交。事务A再次读取数据D,能感知事物B对数据D的改动。也就是说,Read Committed模式下,一个事务既能够感知还有一个事务加入新数据,也能感知这个事务对数据的改动。这个模式也叫
non-repeatable read。Read uncommitted:隔离性最差的一种方式,它是在 Read
committed的基础上又添加了一条破坏事务隔离性的规则。事务A读取了数据D,同一时候数据D被事务B做了改动(事务B还未提交,还在运行过程中);假设事务A再次读取数据D,它将感知数据D被改动了。然后事务B回滚,A持有的数据还是被事务B改动后的。实际数据D未被改动(由于事务B回滚了)。
这样的模式叫”脏读”。
大多数数据库会加入自己定义的隔离性级别, 比如在Oracle、PostgreSQL、SQL Server使用的snapshot Isolation(快照隔离)。许多时间,数据库不会支持SQL规范中定义的全部隔离模式(特别是 Read uncommitted模式)。
用户在连接到数据库时。能够改动默认隔离模式。
二、 Concurrency contro(并发控制)
支撑数据库实现事务隔离性、一致性、原子性的关键是解决好数据库同写的问题(含加入、删除和改动)。
1) 假设全部的事务仅仅是读取数据,他们能并行工作。相互无影响。
2) 假设有一个事务(哪怕仅仅有一个)在改动其他事务读取的数据,数据库须要考虑怎样屏蔽数据改动对其他事务的影响。而且。须要要确保修后的数据不会被其他事务覆盖。
这样的技术称为“并发控制”。
解决问题最简单的方法是让多个事务按时间先后依次运行(串行化)。可是,这是一种很低效的做法(在多核处理器上仅跑一个任务)。
理想的解决方案是随时同意创建事务、运行事务、删除事务。要达到这个目标,须要做到下面几点:
- 实时监控全部事务的全部操作。
- 检查是否存在多个事务同一时候读/写同样数据的情况,是否造成冲突。
- 重排引起冲突的事务运行顺序,将冲突区域范围缩小。
- 按重排好的顺序运行引起数据冲突的操作(不会引起冲突的事务操作仍然并行)。
- 考虑把一些引起冲突的事务取消掉。
本质上来讲,这是一个冲突事务的调度问题。冲突事务调度的算法是很复杂的。也很耗时。
企业级的数据库不可能花费几个小时去寻找最优调度策略。处理冲突事务。
因此。它们仅使用简易的调度策略,使得算法耗费的时间在可接受的范围内。当然这样的调度策略会导致突事务许多其他的时间等待。
三、 Lock manager(锁管理)
为解决事务冲突的问题。大多数数据库使用加锁和数据版本号管理两种策略。这是一个大的命题,我将聚焦在锁管理部分,适当介绍一下数据版本号管理。
Pessimistic lock(悲观锁)。它背后的原理是:
- 假设一个事务须要获取数据。
- 它先将数据加锁。
- 假设还有一个事务也须要获取这块数据。
- 它须要等待第一个事务释放锁。
这样的锁也叫独享锁-exclusive lock。
可是,使用独享锁将导致訪问数据库代价高昂。由于,它要求其他也须要读取同一批数据的事务等待。这也是为什么存在第二种锁—(shared lock)共享锁。
Shared lock(共享锁)。其原理是:
- 假设事务1仅是须要读取数据A。
- 事务1对数据A加shared lock。然后读取数据A。
- 假设事务2也是仅仅须要读取数据A。
- 事务2对数据A加shared lock。然后读取数据A。
- 假设事务3须要改动数据A。
- 事务3对是数据A加Pessimistic lock,它须要等待另外两个事务释放shared lock。
假设一块数据已经加入了Pessimistic lock。另外一个事务即使仅仅是读数据(须要对数据加shared lock),也须要等待Pessimistic lock释放;否则读取的是脏数据。
Lock manager的职责就是管理锁的申请和释放。Lock manager通过哈希表管理锁资源。也管理着锁与数据的关联关系。包含:
- 哪些事务对特定数据加了锁。
- 哪些事务在等待对特定数据加锁。
四、 Dead lock(死锁)
使用锁有可能导致一个问题,即两个事务同一时候等待对方释放锁。
在这张图中。能够看到:
- Transaction A拥有data1的exclusive lock,同一时候申请data2权限。
- Transaction B拥有data2的exclusive lock,同一时候申请data1权限。
这就出现了死锁。
出现死锁时,Lock manger将选择当中一个事务回滚以解除死锁状态。选择哪一个事务回滚,这是个很复杂的问题,要考虑下面方面:
- 回滚涉及数据量最小的事务(造成混滚的代价最小),是否就是最好的决策?
- 回滚最新提交的事务(由于其他事务等待的时间更长),是否就是最好的决策?
- 回滚耗时更短的事务(避免长时间等待。线程饿死)。是否就是最好的决策?
- 即使回滚,又有多少其他事务会受此回滚的影响?
当然,在做出回滚的决策之前。先要明白是否已经出现了事务死锁。
根据lock manager的哈希表,能画出一个依赖关系图(相似上面的截图)。假设在图中出现了环路,即意味着出现了死锁。检查是否出现环路是很耗时的。由于依赖关系图的数据量通常很庞大;所以,一般採用更简单的方法:推断是否超时。假设事务申请的锁未在指定的超时时间内分配。则觉得事务进入了死锁。
Lock manager能够推断新申请的锁是否会导致死锁。同样的。要做出准确的推断,算法也是很耗时间的。取而代之,它採用一些检查条件来推断。
五、 Two-phase locking(二阶段锁)
为确保一个事务全然隔离。最简单方法是在事务開始时申请锁,在事务结束时释放锁。这意味着。事务必须等待申请全然部须要的锁才開始运行。在运行过程中全然占用锁,结束时才统一释放。
这样的方案逻辑上没问题,可是会耗费许多时间在等待锁资源上。
一种更快一些的方案是Two-Phase Locking Protocol(在DB2和SQL Server中使用)。在这样的方案中。一个事务被分解为两个阶段。
- 在growing phase(发展阶段),事务能够申请锁。不能释放锁。
- 在shrinking phase(收缩阶段),事务能够释放锁(已经加锁处理过的数据。且不会再处理),不能申请锁。
其背后的原理有这样两条:
- 尽快释放不再使用的锁。以降低其他事务的等待时间。
- 避免出现这样的情况:某个事务获取数据后,数据又被其他事务改动。以至于数据与获取时不一致。
这样的策略能完美运行,除非一个事务改动了数据,释放了锁。然后又回滚事务。还有一个事务在前一个事务释放锁后。读取了数据;它不清楚改动后的数据后面又发生了回滚。
为了避免出现这样的情况,规定全部的exclusive lock必须在事务结束时才释放。
再多说几句:
当然。一个真实的企业级数据库会使用更复杂的方案,更丰富的锁(如:意向锁),更细的锁控制粒度(基于行、分页、分区、表空间等)。
但,其核心思想是一样的。
这里。我仅仅描写叙述了最基础的锁的原理。Data versioning(数据版本号管理)是还有一种解决事务冲突的方案。
数据版本号管理的基本原理是:
- 全部的事务都能够同一时候改动同样的数据。
- 每一个事务都持有所需数据的一个拷贝(一个版本号)。
假设多个事务改动同样的数据。仅仅有一个事务的改动会被持久化,其他事务的改动会丢弃(事务回滚,后面也可能re-run)。
这样的方式带来性能上的提升。由于:
- 读数据的事务不会堵塞写数据的事务。
- 写数据的事务也不会堵塞读数据的事务。
- 不存在又笨又慢(fat and slow)的锁管理开销。
假设没有出现两个事务同一时候写同一片数据,这样的方式更好。
可是,这样的方式须要巨大的磁盘空间开销。
数据版本号管理和锁管理是两种不同的思想: 乐观锁(optimistic locking)与悲观锁(pessimistic locking)。它们都同一时候存在支持方和反对方,使用哪种方式依赖于详细的引用场景(more reads VS more writes)。说到数据库对Data versioning的支持情况,我觉得PostgreSQL的多版本号数据管理并发控制做得很强大。
一些数据库,如DB2(9.7之前版本号)和SQL Server(除了所谓的视图快照隔离)仅支持加锁的机制。其他一些数据库,如PostgreSQL、MySQL和Oracle同一时候支持加锁和数据版本号管理两种方式。我不知道有什么数据库是仅支持Data versioning的(假设你知道,请告诉我)。
假设你已经读过了介绍隔离性不同级别的章节,就应该清楚。
提升隔离性将添加锁的数量,添加事务申请锁的等待时间。
这也是为什么大多数据库不将隔离性最强的串行化(Serializable)。设置为默认级别的原因。
你也能够在主流的数据库(如MySQL、PostgreSQL、Oracle)指导文档中检查它的设置情况。
已翻译的《How does a relational database work》其他章节链接:
1. 关系型数据库工作原理-时间复杂度:http://blog.csdn.net/ylforever/article/details/51205332
2. 关系型数据库工作原理-归并排序:http://blog.csdn.net/ylforever/article/details/51216916
3. 关系型数据库工作原理-数据结构:http://blog.csdn.net/ylforever/article/details/51278954
4. 关系型数据库工作原理-快速缓存:http://blog.csdn.net/ylforever/article/details/50990121
5. 关系型数据库工作原理-事务管理(一):http://blog.csdn.net/ylforever/article/details/51048945
6. 关系型数据库工作原理-事务管理(二):http://blog.csdn.net/ylforever/article/details/51082294