一.什么是数据库事务
数据库事务是指作为单个逻辑工作单元执行的一系列操作.
设想网上购物的一次交易,其付款过程至少包括以下几步数据库操作:
· 更新客户所购商品的库存信息
· 保存客户付款信息--可能包括与银行系统的交互
· 生成订单并且保存到数据库中
· 更新用户相关信息,例如购物数量等等
正常的情况下,这些操作将顺利进行,最终交易成功,与交易相关的所有数据库信息也成功地更新。但是,如果在这一系列过程中任何一个环节出了差错,例如在更新商品库存信息时发生异常、该顾客银行帐户存款不足等,都将导致交易失败。一旦交易失败,数据库中所有信息都必须保持交易前的状态不变,比如最后一步更新用户信息时失败而导致交易失败,那么必须保证这笔失败的交易不影响数据库的状态--库存信息没有被更新、用户也没有付款,订单也没有生成。否则,数据库的信息将会一片混乱而不可预测。
数据库事务正是用来保证这种情况下交易的平稳性和可预测性的技术。
二.数据事务的ACID属性
事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性:
· 原子性(Atomicity)
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
· 一致性(Consistency)
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。
· 隔离性(Isolation)
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
· 持久性(Durability)
事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
三、事务保存点(SavePoint)
有一个数据库管理员,在早上的时候,设置了一个保存点a1,然后工作(对数据库进行增删改查),到了中午他又设置了一个保存点a2,到了下午他又继续工作(还是对数据库进行增删改查),然后到了晚上。突然之间,他执行了一个很愚蠢的操作,把数据库里的数据全部给删除了!!!!
那这个时候怎么办呢?好在他在早上和中午的时候设置了保存点,然后只要回到保存点就可以了。
rollback to xxxSavePoint
当事务commit之后,事务中设置的SavePoint就取消了
四、事务并发
两个并发事务同时访问数据库表相同的行时,可能存在以下三个问题:
1、幻想读:事务T1读取一条指定where条件的语句,返回结果集。此时事务T2插入一行新记录,恰好满足T1的where条件。然后T1使用相同的条件再次查询,结果集中可以看到T2插入的记录,这条新纪录就是幻想。
2、不可重复读取:事务T1读取一行记录,紧接着事务T2修改了T1刚刚读取的记录,然后T1再次查询,发现与第一次读取的记录不同,这称为不可重复读。
3、脏读:事务T1更新了一行记录,还未提交所做的修改,这个T2读取了更新后的数据,然后T1执行回滚操作,取消刚才的修改,所以T2所读取的行就无效,也就是脏数据。
五、事务的隔离级别
READ UNCOMMITTED 幻想读、不可重复读和脏读都允许。
READ COMMITTED 允许幻想读、不可重复读,不允许脏读
REPEATABLE READ 允许幻想读,不允许不可重复读和脏读
SERIALIZABLE 幻想读、不可重复读和脏读都不允许
Oracle数据库支持READ COMMITTED 和 SERIALIZABLE这两种事务隔离级别。所以Oracle不支持脏读
SQL标准所定义的默认事务隔离级别是SERIALIZABLE,但是Oracle 默认使用的是READ COMMITTED
六、锁
1)说明
锁是一种机制,多个事务同时访问一个数据库对象时,该机制可以实现对并发的控制
2) oracle中锁的类别
1.DDL锁: oracle自动的施加和释放
2.DML锁:事务开始时施加,使用Commit后者Rollback被释放、
3.内部锁: 由oracle自己管理以保护内部数据库结构
3)oracle锁的粒度
1. 行级锁(TX):阻止该行上的DML操作,直到Commit或者Rollback
2. 表级锁(TM):
3. 数据库级锁: eg: 将数据库锁定为只读模式 alter database open read only;
eg: 将数据库设置为限制模式(导入导出数据库时使用):alter system enable restricted session;
4)锁的模式
数字越大,级别越高
5)死锁
1. 说明
1. 当两个用户希望持有对方的资源时就会发生死锁。即两个用户互相等待对方释放资源,oracle认定为产生了死锁,在这种情况下,将以牺牲一个用户作为代价,另一个用户继续执行,牺牲的用户的事务将回滚。
2. lORA-00060的错误并记录在数据库的日志文件alertSID.log中。同时在user_dump_dest下产生了一个跟踪文件,详细描述死锁的相关信息。
2.死锁产生条件
1. Mutual exclusion(互斥):资源不能被共享,只能由一个进程使用。
2. Hold and wait(请求并保持):已经得到资源的进程可以再次申请新的资源。
3. No pre-emption(不可剥夺):已经分配的资源不能从相应的进程中被强制地剥夺。
4. Circular wait(循环等待条件):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。3. 事务和死锁预防总结
1. 避免应用不运行长事务。
2. 经常提交以避免长时间锁定行。
3. 避免使用LOCK命令锁定表。
4. 在非高峰期间执行DDL操作,在非高峰期间执行长时间运行的查询或事务。
1. 认识事务
1.1 为什么需要数据库事务
转账是生活中常见的操作,比如从A账户转账100元到B账号。站在用户角度而言,这是一个逻辑上的单一操作,然而在数据库系统中,至少会分成两个步骤来完成:
- 1.将A账户的金额减少100元
- 2.将B账户的金额增加100元。
在这个过程中可能会出现以下问题:
- 1.转账操作的第一步执行成功,A账户上的钱减少了100元,但是第二步执行失败或者未执行便发生系统崩溃,导致B账户并没有相应增加100元。
- 2.转账操作刚完成就发生系统崩溃,系统重启恢复时丢失了崩溃前的转账记录。
- 3.同时又另一个用户转账给B账户,由于同时对B账户进行操作,导致B账户金额出现异常。
为了便于解决这些问题,需要引入数据库事务的概念。
1.2 什么是数据库事务
定义:数据库事务是构成单一逻辑工作单元的操作集合
一个典型的数据库事务如下所示BEGIN TRANSACTION //事务开始 SQL1 SQL2 COMMIT/ROLLBACK //事务提交或回滚
关于事务的定义有几点需要解释下:
- 1.数据库事务可以包含一个或多个数据库操作,但这些操作构成一个逻辑上的整体。
- 2.构成逻辑整体的这些数据库操作,要么全部执行成功,要么全部不执行。
- 3.构成事务的所有操作,要么全都对数据库产生影响,要么全都不产生影响,即不管事务是否执行成功,数据库总能保持一致性状态。
- 4.以上即使在数据库出现故障以及并发事务存在的情况下依然成立。
1.3 事务如何解决问题
对于上面的转账例子,可以将转账相关的所有操作包含在一个事务中
BEGIN TRANSACTION A账户减少100元 B账户增加100元 COMMIT
- 1.当数据库操作失败或者系统出现崩溃,系统能够以事务为边界进行恢复,不会出现A账户金额减少而B账户未增加的情况。
- 2.当有多个用户同时操作数据库时,数据库能够以事务为单位进行并发控制,使多个用户对B账户的转账操作相互隔离。
事务使系统能够更方便的进行故障恢复以及并发控制,从而保证数据库状态的一致性。
1.4 事务的ACID特性以及实现原理概述
原子性(Atomicity):事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。一致性状态是指:1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等) 2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。
隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
持久性(Durability):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
在事务的ACID特性中,C即一致性是事务的根本追求,而对数据一致性的破坏主要来自两个方面
- 1.事务的并发执行
- 2.事务故障或系统故障
数据库系统是通过并发控制技术和日志恢复技术来避免这种情况发生的。
并发控制技术保证了事务的隔离性,使数据库的一致性状态不会因为并发执行的操作被破坏。
日志恢复技术保证了事务的原子性,使一致性状态不会因事务或系统故障被破坏。同时使已提交的对数据库的修改不会因系统崩溃而丢失,保证了事务的持久性。2.并发异常与并发控制技术
2.1 常见的并发异常
在讲解并发控制技术前,先简单介绍下数据库常见的并发异常。
- 脏写
脏写是指事务回滚了其他事务对数据项的已提交修改,比如下面这种情况在事务1对数据A的回滚,导致事务2对A的已提交修改也被回滚了。
- 丢失更新
丢失更新是指事务覆盖了其他事务对数据的已提交修改,导致这些修改好像丢失了一样。事务1和事务2读取A的值都为10,事务2先将A加上10并提交修改,之后事务2将A减少10并提交修改,A的值最后为,导致事务2对A的修改好像丢失了一样
- 脏读
脏读是指一个事务读取了另一个事务未提交的数据在事务1对A的处理过程中,事务2读取了A的值,但之后事务1回滚,导致事务2读取的A是未提交的脏数据。
- 不可重复读
不可重复读是指一个事务对同一数据的读取结果前后不一致。脏读和不可重复读的区别在于:前者读取的是事务未提交的脏数据,后者读取的是事务已经提交的数据,只不过因为数据被其他事务修改过导致前后两次读取的结果不一样,比如下面这种情况由于事务2对A的已提交修改,事务1前后两次读取的结果不一致。
- 幻读
幻读是指事务读取某个范围的数据时,因为其他事务的操作导致前后两次读取的结果不一致。幻读和不可重复读的区别在于,不可重复读是针对确定的某一行数据而言,而幻读是针对不确定的多行数据。因而幻读通常出现在带有查询条件的范围查询中,比如下面这种情况:事务1查询A<5的数据,由于事务2插入了一条A=4的数据,导致事务1两次查询得到的结果不一样
2.2 事务的隔离级别
事务具有隔离性,理论上来说事务之间的执行不应该相互产生影响,其对数据库的影响应该和它们串行执行时一样。
然而完全的隔离性会导致系统并发性能很低,降低对资源的利用率,因而实际上对隔离性的要求会有所放宽,这也会一定程度造成对数据库一致性要求降低
- SQL标准为事务定义了不同的隔离级别,从低到高依次是
- 读未提交(READ UNCOMMITTED)
- 读已提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 串行化(SERIALIZABLE)
事务的隔离级别越低,可能出现的并发异常越多,但是通常而言系统能提供的并发能力越强。
不同的隔离级别与可能的并发异常的对应情况如下表所示,有一点需要强调,这种对应关系只是理论上的,对于特定的数据库实现不一定准确,比如mysql
的Innodb存储引擎通过Next-Key Locking技术在可重复读级别就消除了幻读的可能。所有事务隔离级别都不允许出现脏写,而串行化可以避免所有可能出现的并发异常,但是会极大的降低系统的并发处理能力。
2.3 事务隔离性的实现——常见的并发控制技术
并发控制技术是实现事务隔离性以及不同隔离级别的关键,实现方式有很多,按照其对可能冲突的操作采取的不同策略可以分为乐观并发控制和悲观并发控制两大类。
乐观并发控制:对于并发执行可能冲突的操作,假定其不会真的冲突,允许并发执行,直到真正发生冲突时才去解决冲突,比如让事务回滚。
悲观并发控制:对于并发执行可能冲突的操作,假定其必定发生冲突,通过让事务等待(锁)或者中止(时间戳排序)的方式使并行的操作串行执行。
2.3.1 基于封锁的并发控制
核心思想:对于并发可能冲突的操作,比如读-写,写-读,写-写,通过锁使它们互斥执行。
锁通常分为共享锁和排他锁两种类型
- 1.共享锁(S):事务T对数据A加共享锁,其他事务只能对A加共享锁但不能加排他锁。
- 2.排他锁(X):事务T对数据A加排他锁,其他事务对A既不能加共享锁也不能加排他锁
基于锁的并发控制流程:
事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排他锁)
申请锁的请求被发送给锁管理器。锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁。
若锁被授予,则申请锁的事务可以继续执行;若被拒绝,则申请锁的事务将进行等待,直到锁被其他事务释放。
可能出现的问题:
死锁:多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行。
饥饿:数据项A一直被加共享锁,导致事务一直无法获取A的排他锁。
对于可能发生冲突的并发操作,锁使它们由并行变为串行执行,是一种悲观的并发控制。
2.3.2 基于时间戳的并发控制
核心思想:对于并发可能冲突的操作,基于时间戳排序规则选定某事务继续执行,其他事务回滚。
系统会在每个事务开始时赋予其一个时间戳,这个时间戳可以是系统时钟也可以是一个不断累加的计数器值,当事务回滚时会为其赋予一个新的时间戳,先开始的事务时间戳小于后开始事务的时间戳。
每一个数据项Q有两个时间戳相关的字段:
W-timestamp(Q):成功执行write(Q)的所有事务的最大时间戳
R-timestamp(Q):成功执行read(Q)的所有事务的最大时间戳时间戳排序规则如下:
假设事务T发出read(Q),T的时间戳为TS
a.若TS(T)<W-timestamp(Q),则T需要读入的Q已被覆盖。此
read操作将被拒绝,T回滚。
b.若TS(T)>=W-timestamp(Q),则执行read操作,同时把
R-timestamp(Q)设置为TS(T)与R-timestamp(Q)中的最大值假设事务T发出write(Q)
a.若TS(T)<R-timestamp(Q),write操作被拒绝,T回滚。
b.若TS(T)<W-timestamp(Q),则write操作被拒绝,T回滚。
c.其他情况:系统执行write操作,将W-timestamp(Q)设置
为TS(T)。基于时间戳排序和基于锁实现的本质一样:对于可能冲突的并发操作,以串行的方式取代并发执行,因而它也是一种悲观并发控制。它们的区别主要有两点:
- 基于锁是让冲突的事务进行等待,而基于时间戳排序是让冲突的事务回滚。
- 基于锁冲突事务的执行次序是根据它们申请锁的顺序,先申请的先执行;而基于时间戳排序是根据特定的时间戳排序规则。
2.3.3 基于有效性检查的并发控制
核心思想:事务对数据的更新首先在自己的工作空间进行,等到要写回数据库时才进行有效性检查,对不符合要求的事务进行回滚。
基于有效性检查的事务执行过程会被分为三个阶段:
读阶段:数据项被读入并保存在事务的局部变量中。所有write操作都是对局部变量进行,并不对数据库进行真正的更新。
有效性检查阶段:对事务进行有效性检查,判断是否可以执行write操作而不违反可串行性。如果失败,则回滚该事务。
写阶段:事务已通过有效性检查,则将临时变量中的结果更新到数据库中。
有效性检查通常也是通过对事务的时间戳进行比较完成的,不过和基于时间戳排序的规则不一样。
该方法允许可能冲突的操作并发执行,因为每个事务操作的都是自己工作空间的局部变量,直到有效性检查阶段发现了冲突才回滚。因而这是一种乐观的并发策略。
2.3.4 基于快照隔离的并发控制
快照隔离是多版本并发控制(mvcc)的一种实现方式。
其核心思想是:数据库为每个数据项维护多个版本(快照),每个事务只对属于自己的私有快照进行更新,在事务真正提交前进行有效性检查,使得事务正常提交更新或者失败回滚。
由于快照隔离导致事务看不到其他事务对数据项的更新,为了避免出现丢失更新问题,可以采用以下两种方案避免:
先提交者获胜:对于执行该检查的事务T,判断是否有其他事务已经将更新写入数据库,是则T回滚否则T正常提交。
先更新者获胜:通过锁机制保证第一个获得锁的事务提交其更新,之后试图更新的事务中止。
事务间可能冲突的操作通过数据项的不同版本的快照相互隔离,到真正要写入数据库时才进行冲突检测。因而这也是一种乐观并发控制。
2.3.5 关于并发控制技术的总结
以上只是对常见的几种并发控制技术进行了介绍,不涉及特别复杂的原理的讲解。之所以这么做一是要真的把原理和实现细节讲清楚需要涉及的东西太多,篇幅太长,从作者和读者角度而言都不是一件轻松的事,所以只对其实现的核心思想和实现要点进行了简单的介绍,其他部分就一笔带过了。二是并发控制的实现的方式太过多样,基于封锁的实现就有很多变体,mvcc多版本并发控制的实现方式就更是多样,而且很多时候会和其他并发控制方式比如封锁的方式结合起来使用。
3. 故障与故障恢复技术
3.1 为什么需要故障恢复技术
数据库运行过程中可能会出现故障,这些故障包括事务故障和系统故障两大类
- 事务故障:比如非法输入,系统出现死锁,导致事务无法继续执行。
- 系统故障:比如由于软件漏洞或硬件错误导致系统崩溃或中止。
这些故障可能会对事务和数据库状态造成破坏,因而必须提供一种技术来对各种故障进行恢复,保证数据库一致性,事务的原子性以及持久性。数据库通常以日志的方式记录数据库的操作从而在故障时进行恢复,因而可以称之为日志恢复技术。
3.2 事务的执行过程以及可能产生的问题
事务的执行过程可以简化如下:
系统会为每个事务开辟一个私有工作区
事务读操作将从磁盘中拷贝数据项到工作区中,在执行写操作前所有的更新都作用于工作区中的拷贝.
事务的写操作将把数据输出到内存的缓冲区中,等到合适的时间再由缓冲区管理器将数据写入到磁盘。
由于数据库存在立即修改和延迟修改,所以在事务执行过程中可能存在以下情况:
- 在事务提交前出现故障,但是事务对数据库的部分修改已经写入磁盘数据库中。这导致了事务的原子性被破坏。
- 在系统崩溃前事务已经提交,但数据还在内存缓冲区中,没有写入磁盘。系统恢复时将丢失此次已提交的修改。这是对事务持久性的破坏。
3.3 日志的种类和格式
<T,X,V1,V2>
:描述一次数据库写操作,T是执行写操作的事务的唯一标识,X是要写的数据项,V1是数据项的旧值,V2是数据项的新值。
<T,X,V1>
:对数据库写操作的撤销操作,将事务T的X数据项恢复为旧值V1。在事务恢复阶段插入。
<T start>
: 事务T开始
<T commit>
: 事务T提交
<T abort>
: 事务T中止关于日志,有以下两条规则
- 1.系统在对数据库进行修改前会在日志文件末尾追加相应的日志记录。
- 2.当一个事务的commit日志记录写入到磁盘成功后,称这个事务已提交,但事务所做的修改可能并未写入磁盘
3.4 日志恢复的核心思想
撤销事务undo:将事务更新的所有数据项恢复为日志中的旧值,事务撤销完毕时将插入一条
<T abort>
记录。重做事务redo:将事务更新的所有数据项恢复为日志中的新值。
事务正常回滚/因事务故障中止将进行redo
系统从崩溃中恢复时将先进行redo再进行undo。以下事务将进行undo:日志中只包括
<T start>
记录,但既不包括<T commit>
记录也不包括<T abort>
记录.以下事务将进行redo:日志中包括
<T start>
记录,也包括<T commit>
记录或<T abort>
记录。假设系统从崩溃中恢复时日志记录如下
<T0 start> <T0,A,1000,950> <T0,B,2000,2050> <T0 commit> <T1 start> <T1,C,700,600>
由于T0既有start记录又有commit记录,将会对事务T0进行重做,执行相应的redo操作。
由于T1只有start记录,将会对T1进行撤销,执行相应的undo操作,撤销完毕将写入一条abort记录。3.5 事务故障中止/正常回滚的恢复流程
从后往前扫描日志,对于事务T的每个形如
<T,X,V1,V2>
的记录,将旧值V1写入数据项X中。往日志中写一个特殊的只读记录
<T,X,V1>
,表示将数据项恢复成旧值V1,
这是一个只读的补偿记录,不需要根据它进行undo。一旦发现了
<T start>
日志记录,就停止继续扫描,并往日志中写一个<T abort>
日志记录。3.6 系统崩溃时的恢复过程(带检查点)
检查点是形如
<checkpoint L>
的特殊的日志记录,L是写入检查点记录时还未提交的事务的集合,系统保证在检查点之前已经提交的事务对数据库的修改已经写入磁盘,不需要进行redo。检查点可以加快恢复的过程。系统奔溃时的恢复过程分为两个阶段:重做阶段和撤销阶段。
重做阶段:
系统从最后一个检查点开始正向的扫描日志,将要重做的事务的列表undo-list设置为检查点日志记录中的L列表。
发现
<T,X,V1,V2>
的更新记录或<T,X,V>
的补偿撤销记录,就重做该操作。发现
<T start>
记录,就把T加入到undo-list中。发现
<T abort>
或<T commit>
记录,就把T从undo-list中去除。撤销阶段:
系统从尾部开始反向扫描日志
发现属于undo-list中的事务的日志记录,就执行undo操作
发现undo-list中事务的T的
<T start>
记录,就写入一条<T abort>
记录,
并把T从undo-list中去除。4.undo-list为空,则撤销阶段结束
总结:先将日志记录中所有事务的更新按顺序重做一遍,在针对需要撤销的事务按相反的顺序执行其更新操作的撤销操作。
3.6.1 一个系统崩溃恢复的例子
恢复前的日志如下,写入最后一条日志记录后系统崩溃
<T0 start> <T0,B,2000,2050> <T2 commit> <T1 start> <checkpoint {T0,T1}> //之前T2已经commit,故不用重做 <T1,C,700,600> <T1 commit> <T2 start> <T2,A,500,400> <T0,B,2000> <T0 abort> //T0回滚完成,插入该记录后系统崩溃
4. 总结
事务是数据库系统进行并发控制的基本单位,是数据库系统进行故障恢复的基本单位,从而也是保持数据库状态一致性的基本单位。ACID是事务的基本特性,数据库系统是通过并发控制技术和日志恢复技术来对事务的ACID进行保证的,从而可以得到如下的关于数据库事务的概念体系结构。
5. 参考资料
《数据库系统概论》