1. 事务的理解
事务由一组操作构成,这组操作必须全部执行,只要任意一个步骤出错,就需要回滚到之前的操作
一个典型的数据库事务如下所示:
BEGIN TRANSACTION //事务开始
SQL1
SQL2
COMMIT/ROLLBACK //事务提交或回滚
关于事务的定义有几点需要解释下:
- 1.数据库事务可以包含一个或多个数据库操作,但这些操作构成一个逻辑上的整体。
- 2.构成逻辑整体的这些数据库操作,要么全部执行成功,要么全部不执行。
- 3.构成事务的所有操作,要么全都对数据库产生影响,要么全都不产生影响,即不管事务是否执行成功,数据库总能保持一致性状态。
- 4.以上即使在数据库出现故障以及并发事务存在的情况下依然成立。
事务如何解决问题:
BEGIN TRANSACTION
A账户减少100元
B账户增加100元
COMMIT
- 1.当数据库操作失败或者系统出现崩溃,系统能够以事务为边界进行恢复,不会出现A账户金额减少而B账户未增加的情况。
- 2.当有多个用户同时操作数据库时,数据库能够以事务为单位进行并发控制,使多个用户对B账户的转账操作相互隔离。
事务使系统能够更方便的进行故障恢复以及并发控制,从而保证数据库状态的一致性。
1.1 四大特性:
- 原子性
原子性是指事务包含的所有操作要么全部成功,要么全部回滚 - 一致性
事务执行之前和之后必须都处于一致性状态
一致性状态是指:1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等) 2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。
比如A和B的账户一共有1000,不管A和B之间怎么互相转帐,最终他们两个的账户加起来还是1000,不能多也不能少,若不是1000就不满足一致性状态了 - 隔离性
当多个用户并发的访问数据库时,比如他们操作同一张表时,数据库为每个用户开启的事务不能被其他事务操作所干扰,多个并发事务之间要相互隔离。
也就是事务的执行是相互独立的,不互相干扰,一个事务看不到另一个正在运行的事务的数据 - 持久性
一个事务一旦被提交了,那么对数据库的改变就是永久的,即使数据库系统遇到故障,也不会丢失已提交的事务的操作
一致性是事务的根本追求,而对数据一致性的破坏主要来自两个方面:
- 1.事务的并发执行
- 2.事务故障或系统故障
数据库系统是通过并发控制技术和日志恢复技术来避免这种情况发生的。
并发控制技术保证了事务的隔离性,使数据库的一致性状态不会因为并发执行的操作被破坏。
日志恢复技术保证了事务的原子性,使一致性状态不会因事务或系统故障被破坏。同时使已提交的对数据库的修改不会因系统崩溃而丢失,保证了事务的持久性。
1.2 数据库常见的并发异常
- 脏写
是指事务回滚了其他事务对数据项的已提交修改,比如下面这种情况 - 丢失更新
是指事务覆盖了其他事务对数据的已提交修改,导致这些修改好像丢失了一样 - 脏读
是指一个事务读取了另一个未提交事务的数据
比如A向B转账100,它有两个操作B+100,A-100;当执行完B+100后,如果此时A通知B查看账户,B一看发现自己确实多了100,但是这个事务还没提交,只要这个事务不提交,不管第二个操作执行不执行,只要事务不提交,所有操作都会回滚;当B再次看它的钱是发现不见了100。这个例子就是B读取了未提交事务的数据导致。 - 不可重复读
对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的值,这是因为在查询的间隙被另一个事务修改并提交成功了。
和脏读的区别:
脏读:一个事务读取了另一个未提交事务的数据
不可重复读:一个事务读取了前一个已提交事务的数据
- 虚读
例:事务T1对一个表中的所有行的某一列做了修改操作,假设是把 1 改为 2,此时,事务T2对这个表插入一条数据并提交给数据库,但这条数据还是1,此时操作T1的用户查看刚刚修改的数据,发现有一行没修改,就像产生幻觉一样,但其实他修改了,这是T2添加的。这种情况旧发生例虚读。
和不可重复读的区别:
他们都是读取了另一个已提交事务的数据,这点和脏读不同
不同的是
不可重复读查询的是同一个数据项,针对的是一个数据
虚度是针对的是一批数据整体
1.3 事务的隔离级别
- Read Uncommited(读未提交)
一个事务可以读取另一个事务未提交的数据,级别最低,上述的任何情况都不能保证 - Read Commited(读已提交)
一个事务要等另一个事务提交后才能读取数据,可以避免脏读。 - Repeatable Read(可重复读)
在事务执行期间会锁定该事务以任何方式引用的所有行,可避免脏读,不可重复读。mysql默认的隔离级别 - Serializable (序列化,串行化)
事务顺序执行;级别最高,可避免脏读,不可重复读,虚读;性能最低,花费大,一般很少使用
上述四种级隔离级别,它们的级别从上到下递增,级别越高,性能越低。
mysql设置隔离级别:
- 查看
SELECT @@tx_isolation
- 设置
set [ session | global ] transaction isolation level 隔离级别名称
set tx_isolation=‘隔离级别名称’
设置一定要在开启事务之前
2. 原子性的实现
为了保证事务操作的原子性,必须实现基于日志的REDO/UNDO机制
- REDO:
重演将所有已经执行成功但尚未写入到磁盘的操作,保证持久性。
将事务更新的所有数据项恢复为日志中的旧值,事务撤销完毕时将插入一条记录 - UNDO:
撤销所有执行了一部分但尚未提交的操作,保证原子性
crash recovery:崩溃恢复
为了实现原子性,需要通过日志:将所有对数据的更新操作都写入日志,如果一个事务中的一部分操作已经成功,但以后的操作,由于断电/系统崩溃/其它的软硬件错误而无法继续,则通过回溯日志,将已经执行成功的操作撤销,从而达到“全部操作失败”的目的。最常见的场景是,数据库系统崩溃后重启,此时数据库处于不一致的状态,必须先执行一个crash recovery的过程:读取日志进行REDO,再对所有到崩溃时尚未成功提交的事务进行UNDO。crash recovery结束后,数据库恢复到一致性状态,可以继续被使用。
3. 隔离性的实现-----常见的并发控制技术
在多个事务并行进行的情况下,即使保证了每一个事务的原子性,仍然可能导致数据不一致的结果,所以引入隔离性
并发控制技术是实现事务隔离性以及不同隔离级别的关键,实现方式有很多,按照其对可能冲突的操作采取的不同策略可以分为乐观并发控制和悲观并发控制两大类。
-
乐观并发控制
对于并发执行可能冲突的操作,假定其不会真的冲突,允许并发执行,直到真正发生冲突时才去解决冲突,比如让事务回滚。 -
悲观并发控制
对于并发执行可能冲突的操作,假定其必定发生冲突,通过让事务等待(锁)或者中止(时间戳排序)的方式使并行的操作串行执行。
3.1 基于封锁的并发控制
核心思想:对于并发可能冲突的操作,比如读-写,写-读,写-写,通过锁使它们互斥执行。
锁通常分为共享锁和排他锁两种类型:
1.共享锁(S):事务T对数据A加共享锁,其他事务只能对A加共享锁但不能加排他锁。
2.排他锁(X):事务T对数据A加排他锁,其他事务对A既不能加共享锁也不能加排他锁
基于锁的并发控制流程:
- 事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排他锁)
- 申请锁的请求被发送给锁管理器。锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁。
- 若锁被授予,则申请锁的事务可以继续执行;
- 若被拒绝,则申请锁的事务将进行等待,直到锁被其他事务释放。
可能出现的问题:
死锁:多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行。
饥饿:数据项A一直被加共享锁,导致事务一直无法获取A的排他锁。
对于可能发生冲突的并发操作,锁使它们由并行变为串行执行,是一种悲观的并发控制。
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)。
基于时间戳排序和基于锁实现的本质一样:对于可能冲突的并发操作,以串行的方式取代并发执行,因而它也是一种悲观并发控制。它们的区别主要有两点:
基于锁是让冲突的事务进行等待,而基于时间戳排序是让冲突的事务回滚。
基于锁冲突事务的执行次序是根据它们申请锁的顺序,先申请的先执行;而基于时间戳排序是根据特定的时间戳排序规则。
3.3 基于有效性检查的并发控制
核心思想:事务对数据的更新首先在自己的工作空间进行,等到要写回数据库时才进行有效性检查,对不符合要求的事务进行回滚。
基于有效性检查的事务执行过程会被分为三个阶段:
-
- 读阶段:数据项被读入并保存在事务的局部变量中。所有write操作都是对局部变量进行,并不对数据库进行真正的更新。
-
- 有效性检查阶段:对事务进行有效性检查,判断是否可以执行write操作而不违反可串行性。如果失败,则回滚该事务。
-
- 写阶段:事务已通过有效性检查,则将临时变量中的结果更新到数据库中。
有效性检查通常也是通过对事务的时间戳进行比较完成的,不过和基于时间戳排序的规则不一样。
该方法允许可能冲突的操作并发执行,因为每个事务操作的都是自己工作空间的局部变量,直到有效性检查阶段发现了冲突才回滚。因而这是一种乐观的并发策略。
3.4 基于快照隔离的并发控制
快照隔离是多版本并发控制(mvcc)的一种实现方式。
其核心思想是:数据库为每个数据项维护多个版本(快照),每个事务只对属于自己的私有快照进行更新,在事务真正提交前进行有效性检查,使得事务正常提交更新或者失败回滚。
由于快照隔离导致事务看不到其他事务对数据项的更新,为了避免出现丢失更新问题,可以采用以下两种方案避免:
-
先提交者获胜:对于执行该检查的事务T,判断是否有其他事务已经将更新写入数据库,是则T回滚否则T正常提交。
-
先更新者获胜:通过锁机制保证第一个获得锁的事务提交其更新,之后试图更新的事务中止。
事务间可能冲突的操作通过数据项的不同版本的快照相互隔离,到真正要写入数据库时才进行冲突检测。因而这也是一种乐观并发
4. 故障与故障恢复技术
数据库运行过程中可能会出现故障,这些故障包括事务故障和系统故障两大类
- 事务故障:比如非法输入,系统出现死锁,导致事务无法继续执行。
- 系统故障:比如由于软件漏洞或硬件错误导致系统崩溃或中止。
这些故障可能会对事务和数据库状态造成破坏,因而必须提供一种技术来对各种故障进行恢复,保证数据库一致性,事务的原子性以及持久性。数据库通常以日志的方式记录数据库的操作从而在故障时进行恢复,因而可以称之为日志恢复技术。
4.1 事务的执行过程以及可能产生的问题
事务的执行过程可以简化如下:
-
系统会为每个事务开辟一个私有工作区
-
事务读操作将从磁盘中拷贝数据项到工作区中,在执行写操作前所有的更新都作用于工作区中的拷贝.
-
事务的写操作将把数据输出到内存的缓冲区中,等到合适的时间再由缓冲区管理器将数据写入到磁盘。
由于数据库存在立即修改和延迟修改,所以在事务执行过程中可能存在以下情况:
在事务提交前出现故障,但是事务对数据库的部分修改已经写入磁盘数据库中。这导致了事务的原子性被破坏。
在系统崩溃前事务已经提交,但数据还在内存缓冲区中,没有写入磁盘。系统恢复时将丢失此次已提交的修改。这是对事务持久性的破坏。
4.2
日志格式:
关于日志,有以下两条规则:
- 1.系统在对数据库进行修改前会在日志文件末尾追加相应的日志记录。
- 2.当一个事务的commit日志记录写入到磁盘成功后,称这个事务已提交,但事务所做的修改可能并未写入磁盘
日志恢复的核心思想:
- 撤销事务undo:将事务更新的所有数据项恢复为日志中的旧值,事务撤销完毕时将插入一条
< T abort >记录。 - 重做事务redo:将事务更新的所有数据项恢复为日志中的新值。
参考文章: https://www.cnblogs.com/takumicx/p/9998844.html
他写的特别好