但行好事
莫论前程❤

数据库事务详解

大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。
MySQL的默认隔离级别是Repeatable read。

事务

事务应该说是数据库最核心的能力之一,对于任何和数据打交道的开发人员而言,是非常重要的.

事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。

基本特征

  • ACID 事务具有4个基本特征,分别是:
    • 原子性(Atomicity)、
    • 一致性(Consistency)、
    • 隔离性(Isolation)、
    • 持久性(Duration),简称ACID。

隔离级别

ACID这4个特征中,最难理解的是隔离性。在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。

4个隔离级别分别是:

  • 读未提及(READ_UNCOMMITTED)、
  • 读已提交(READ_COMMITTED)、
  • 可重复读(REPEATABLE_READ)、
  • 顺序读(SERIALIZABLE)。

事务并发引起的问题

​ 数据库在不同的隔离性级别下并发访问可能会出现以下几种问题:

  • 脏读(Dirty Read)、
  • 不可重复读(Unrepeatable Read)、
  • 幻读(Phantom Read)。

事务的思维导图

img

ACID

1. 原子性(Atomicity)

事务的原子性是指事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,只允许出现两种状态之一。

  • 全部执行成功
  • 全部执行失败

任何一项操作都会导致整个事务的失败,同时其它已经被执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成。

2. 一致性(Consistency)

事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处以一致性状态。

比如:如果从A账户转账到B账户,不可能因为A账户扣了钱,而B账户没有加钱。

  1. 隔离性(Isolation)

事务的隔离性是指在并发环境中,并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。

一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务是不能互相干扰的。

隔离性分4个级别,下面会介绍。

4. 持久性(Duration)

事务的持久性是指事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。

事务隔离级别

1. 读未提及(READ_UNCOMMITTED)

读未提及,该隔离级别允许脏读取,其隔离级别是最低的。换句话说,如果一个事务正在处理某一数据,并对其进行了更新,但同时尚未完成事务,因此还没有提交事务;而以此同时,允许另一个事务也能够访问该数据。

脏读示例:

在事务A和事务B同时执行时可能会出现如下场景:

时间事务A(存款)事务B(取款)
T1开始事务——
T2——开始事务
T3——查询余额(1000元)
T4——取出1000元(余额0元)
T5查询余额(0元)——
T6——撤销事务(余额恢复1000元)
T7存入500元(余额500元)——
T8提交事务——

余额应该为1500元才对。请看T5时间点,事务A此时查询的余额为0,这个数据就是脏数据,他是事务B造成的,很明显是事务没有进行隔离造成的。

2. 读已提交(READ_COMMITTED)

读已提交是不同的时候执行的时候只能获取到已经提交的数据。
这样就不会出现上面的脏读的情况了。

不可重复读示例

可是解决了脏读问题,但是还是解决不了可重复读问题。

时间事务A(存款)事务B(取款)
T1开始事务——
T2——开始事务
T3——查询余额(1000元)
T4查询余额(1000元)——
T5——取出1000元(余额0元)
T6——提交事务
T7查询余额(0元)——
T8提交事务——

事务A其实除了查询两次以外,其它什么事情都没做,结果钱就从1000编程0了,这就是不可重复读的问题。

3. 可重复读(REPEATABLE_READ)

可重复读就是保证在事务处理过程中,多次读取同一个数据时,该数据的值和事务开始时刻是一致的。因此该事务级别进制了不可重复读取和脏读,但是有可能出现幻读的数据。

幻读

幻读就是指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。

时间事务A(统计总存款)事务B(存款)
T1开始事务——
T2——开始事务
T3统计总存款(1000元)——
T4——存入100元
T5——提交事务
T6提交总存款(10100)——
T7提交事务——

银行工作人员在一个事务中多次统计总存款时看到结果不一样。如果要解决幻读,那只能使用顺序读了。

4. 顺序读(SERIALIZABLE)

顺序读是最严格的事务隔离级别。它要求所有的事务排队顺序执行,即事务只能一个接一个地处理,不能并发。

事务隔离级别对比

事务隔离级别脏 读不可重复读幻 读
读未提及(READ_UNCOMMITTED)允许允许允许
读已提交(READ_COMMITTED)禁止允许允许
可重复读(REPEATABLE_READ)禁止禁止允许
顺序读(SERIALIZABLE)禁止禁止禁止

4种事务隔离级别从上往下,级别越高,并发性越差,安全性就越来越高。
一般数据默认级别是读以提交或可重复读。

事务的状态

因为事务具有原子性,所以从远处看的话,事务就是密不可分的一个整体,事务的状态也只有三种:Active、Commited 和 Failed,事务要不就在执行中,要不然就是成功或者失败的状态:

commited-->active-->failed

但是如果放大来看,我们会发现事务不再是原子的,其中包括了很多中间状态,比如部分提交,事务的状态图也变得越来越复杂。
事务状态
– Active:事务的初始状态,表示事务正在执行;
– Partially Commited:在最后一条语句执行之后;
– Failed:发现事务无法正常执行之后;
– Aborted:事务被回滚并且数据库恢复到了事务进行之前的状态之后;
– Commited:成功执行整个事务;

事务的分类

从事务理论的角度可以把事务分为以下几种类型:

  • 扁平事务(Flat Transactions)
  • 带有保存节点的扁平事务(Flat Transactions with Savepoints)
  • 链事务(Chained Transactions)
  • 嵌套事务(Nested Transactions)
  • 分布式事务(Distributed Transactions)
扁平事务

扁平事务(Flat Transactions)是事务类型中最简单但使用最频繁的事务。在扁平事务中,所有的操作都处于同一层次,由BEGIN/START TRANSACTION开始事务,由COMMIT/ROLLBACK结束,且都是原子的,要么都执行,要么都回滚。因此扁平事务是应用程序成为原子操作的基本组成模块。扁平事务一般有三种不同的结果:
1.事务成功完成。在平常应用中约占所有事务的96%。
1

2.应用程序要求停止事务。比如应用程序在捕获到异常时会回滚事务,约占事务的3%。
2

3.外界因素强制终止事务。如连接超时或连接断开,约占所有事务的1%。
3

扁平事务的主要限制是不能提交或者回滚事务的某一部分。如果某一事务中有多个操作,在一个操作有异常时并不希望之的操作全部回滚,而是保存前面操作的更改。扁平事务并不能支持这样的事例,因此就出现了带有保存节点的扁平事务。

带有保存节点的扁平事务

带有保存节点的扁平事务(Flat Transactions with Savepoints)允许事务在执行过程中回滚到较早的一个状态,而不是回滚所有的操作。保存点(Savepoint)用来通知系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。

对于扁平事务来说,在事务开始时隐式地设置了一个保存点,回滚时只能回滚到事务开始时的状态。下图是回滚到某个保存节点的实例:
扁平事务

链事务

链事务(Chained Transaction)是指一个事务由多个子事务链式组成。前一个子事务的提交操作和下一个子事务的开始操作合并成一个原子操作,这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的一样。这样,在提交子事务时就可以释放不需要的数据对象,而不必等到整个事务完成后才释放。其工作方式如下:
链事务

嵌套事务

嵌套事务(Nested Transaction)是一个层次结构框架。由一个顶层事务(top-level transaction)控制着各个层次的事务。顶层事务之下嵌套的事务成为子事务(subtransaction),其控制着每一个局部的操作,子事务本身也可以是嵌套事务。因此,嵌套事务的层次结构可以看成是一颗树.
嵌套事务

分布式事务

分布式事务(Distributed Transactions)通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中不同节点的数据库资源。
例如一个银行用户从招商银行的账户向工商银行的账户转账1000元,这里需要用到分布式事务,因为不能仅调用某一家银行的数据库就完成任务。
分布式事务

事务的原子性

事务的最基本功能是原子性。 比如张三给李四异地打钱5000元,假设同一银行异地手续费是5‰,那么数据库要干三件事情  张三的账户余额扣除5025(含5‰手续费,中国特色)  李四的账户余额增加5000  银行自己的账户余额增加25  这三件事情要么全部成功,要么全部失败,绝对不能一些成功,一些失败。  本地事务

 String sql = 
“update Account set Balance = Balance + ? where id=?”
    try (Connection con = dataSource.getConnection();
PreparedStatement pstmtForSource = con.preparedStatement(sql);
PreparedStatement pstmtForTarget = con.preparedStatement(sql);
PreparedStatement pstmtForBlank = con.preparedStatement(sql)) {
        con.setAutoCommit(false); //关闭自动提交,手动事务开始
        pstmtForSource.setInt(1, -5025);
        pstmtForSource.setLong(2, sourceAccountId);
        pstmtForSource.executeUpdate();

        pstmtForTarget.setInt(1, +5000);
        pstmtForTarget.setLong(2, targetAccountId);
        pstmtForTarget.executeUpdate();

        pstmtForBank.setInt(1, +25);
        pstmtForBank.setLong(2, 1L);银行自己卡号为1
        pstmtForBank.executeUpdate();
        con.commit(); //提交事务
} catch (SQLException | RuntimeException | Error ex) {
    con.rollback(); //回滚事务
    throw ex; //不要忽略,继续抛出,让ATM界面层报错
}

​ 数据库连接使用setAutoCommit(false)来开始一个事务,此所做的所有事情都是原子性事务的一部分,最后一件事情做完后,调用con.commit来提交事务。如果整个过程有任何异常发生,可以调用con.rollback()来撤销已经被执行的那部分修改。

​ 数据库连接的自动提交默认为true,自动提交为true的意思就是每句SQL执行完成后,数据库都会自动根据成功与否来提交或回滚。这是毫无意义的,事务的原子性只有对多个操作而言才有意义,要么全部成功要么全部失败这句话本身就隐含整个过程还有多个SQL操作的意思。所谓,默认的自动提交也可以理解成无事务的意思。

​ 一旦setAutoCommit(false);就表示数据库开启一个需要手动提交或回滚的事务,从这句话开始,一直往后,到最接近的commit或rollback调用的代码之间,所执行的任何SQL修改都作为一个不可分割的一个整体,那理论性点的话说,就是一个原子。原子中所有语句要么都成功,要么都失败。

​ 特殊地,如果因为网络故障、客户端崩溃或者数据库本身崩溃而导致既没有commit也没有rollback。等数据库察觉到这个异常情况后,都视为rollback。

​ 一旦commit或rollback之后,下一个的事务又自动开始了。当前事务的最终结果已经成事实了,板上钉钉了。更后面的提交或回滚的调用只针对下一个事务。从这里,你也可以往下延伸,即同一个connection 上可以执行多个事务,在connection close之前,你有多少个commit就代表你提交了多少个事务。

保存点

​ 数据库事务回滚默认是整体回滚,即回滚到事务刚开始的地方,这样做是为了保证原子性 。但数据库也提供一种故意破坏原子性的功能,叫做保存点(Save Point),保存点可以使用专用的SQL语句当前事务添加注册。事务开始后,添加保存点的SQL和操作数据的SQL可以任意混合地不断执行,但在当前事务范围内,各保存点的名称必须唯一,这样,多个保存点可以把很多个数据操作SQL的分成很多小段。最后可以使用指定一个保存点名称的rollback操作,这样,就可以回滚到添加那个保存点的SQL的位置,而不是默认的全部回滚。  数据库支持此功能,JDBC也支持暴露数据库的这个能力,所以大家还是有必要了解这个概念。但说实话,用得非常少,应用场景不多。

扁平事务和嵌套事务

​ 对于所有数据库而言,针对一个连接,事务的扁平结构是默认结构,结束上一个事务隐含了下一个事务的开始。事务总是被开始、结束、开始、结束,同一时刻,一个连接顶多能开启一个事务。这种事务模型为扁平事务。而对少数数据库而言,针对一个连接,事务总是被开始、开始、结束、结束,但可能需要该数据产品特有的特殊的SQL命令。这是开启了一个父事务和子事务,父事务和子事务各自遵循自己的原子性,双方的提交回滚彼此不干扰。这就是嵌套事务。这个概念,有点类似spring里面的Nested事务,但这里是数据库层面的,而且是针对同一个连接,对于绝大多数仅仅支持扁平事务的数据库而言,可以让当前线程创建两个不同的数据库连接,然后在两个不同的连接上各开启一个事务,属于不同连接的不同事务各自遵循自己的原子性,各自的提交回滚彼此不干扰。这是扁平事务数据库模拟嵌套事务的一个经典用法。也是事务传播属性里,require new和nested的实现原理。

数据库事务实现大致原理

以Oracle为例,Oracle数据都存储在表空间上,表空间里面有一个段,叫做Undo段,在一个事务中,所进行的所有增删改操作被实施之前,都先要按照严格的顺序在Undo段保持每条记录的旧数据(对于INSERT操作而言,旧数据为空),这样这对数据修改之前,Undo段就保证备份了所有被操作记录的原数据。如果最终被提交,清空Undo段中的数据,如果最终rollback,则按照Undo中事先备份好的原数据进行逆向操作,每完成一项逆向操作,就清除一部分Undo数据,最后全部回滚后,Undo段的数据也被清空了。

而在 MySQL 中,恢复机制是通过回滚日志(undo log)实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。

​ 如果网络掉线或客户端崩溃,一定超时后,数据库能发现超时的“死链接”,数据库会清除死链接,并且解开死连接所持有的锁,并且根据和死连接相关联的Undo段数据开始逆向操作以撤销修改。

​ 如果数据库本身崩溃、数据库所在操作系统奔溃、服务器硬件故障或者服务器停电导致数据库死掉。人工采取恢复措施(例如换主板、或想办法恢复电力供给)后重启数据库,刚重启的数据库会拒绝所有客户的连接申请,专心看储存介质上是否有Undo数据,如果有,开始撤销,每撤销一点就清除一点Undo数据。考虑更极端一点,如果在撤销了一部分后,数据库又出问题,那么大不了再重启一次再来,反正还没有被用于逆操作的Undo数据还在,当所有的Undo数据被全部清空后,意味着所有的未提交操作全部非法数据都被逆操作了。这是标志着数据库得以全部恢复,自此,数据库服务器才开始接受外界申请连接,进入正常的服务状态。

​ 总之,只要存储数据的存储介质本身没有损坏,无论多极端的软件或硬件故障,数据库一定能回滚。而事实上,存储介质本身也很可能有硬件层面的有镜像容错能力,这就如虎添翼,更完美了。

Undo段故障

​ 如果启动一个过于庞大的事务,事务开始之后到提交之前的修改行为过于海量,当会导致Oracle表空间Undo段所允许储存资源被耗尽,此时应用程序会得到异常。出现这个问题后,要仔细分析问题,辨别是应用程序写得太二(比如可以用小一点的事务实现同样的功能)还是数据库配置太二。最终决定由开发人员改应用程序还是由DBA改数据库软硬件设置。

事务隔离级别

上面所讲的事务的原子性,是对多条修改SQL具备意义。对于读操作,事务同样具备重大意义,这就是事务隔离级别  SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

Read Uncommitted(读取未提交内容)

​ 特别提醒,Oracle不支持此级别!在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少,但读取到的数据极其不靠谱。读取未提交的数据,可能前脚刚读到别人修改但未提交的数据,后脚数据就被别人回滚撤销了,自己读到了一份完全无效的数据还浑然不知,这种最无节操的问题称之为脏读(Dirty Read)。

Read Committed(读取提交内容)

​ 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。这个级别可以解决脏读(Dirty Read)的问题,一个事务只能看见已经提交事务所做的改变,如果其它事务反复修改数据,当前事务多次读取同一条数据每次会读到不同的数据,这种现象叫做不可重复读(Nonrepeatable Read)。

Repeatable Read(可重读)

​ 特别提醒,Oracle不支持此级别!这是MySQL的默认事务隔离级别。这个级别可以解决不可重复读的(Nonrepeatable Read)问题。它确保同一事务的多次同一条数据的时候,每次会看到同样的数据行。 但是其它事务任然还是可以添加和删除同一张表的其它数据,导致当前事务反复看这张表的记录总条数,有时变多有时变少,就如同看街上闪烁的霓虹灯一样,这种问题叫做幻读(Phantom Read)

Serializable(串行化读)

​ 这是最高的隔离级别,连幻读(Phantom Read)问题也被解决了。所有企图操作同一张表(无论读写)的事务必须割舍掉所有并发性,串行化地排队。对一张表而言,此级别完全不具备任何并发性,读取到的数据绝对可靠。

隔离级别表格总结

1530584577564

越靠上,读取到的数据越不严密,但并发度越高。  越靠下,读取到的数据越严密,但并发度越低下。  典型的鱼和熊掌难以兼得的问题,就连数据库制造商自己都觉得难以取舍,就给了这个4档变速箱,开发人员根据实际路况(项目具体情况)自己选。

隔离级别基本原理

​ 由于部分数据库对4种级别支持得未必全,比如Oracle就仅仅支持两个级别,而且每种数据库的实现细节会稍微有所差异,所以我们讲解一种理论上最简实现原理。实际数据库实现完整隔离级别的原理只能比这个模型更复杂,不能更简单。

赞(0) 打赏
未经允许不得转载:刘鹏博客 » 数据库事务详解
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏