事务四大特征:原子性,一致性,隔离性和持久性(ACID)

2018-01-11 12:50:14来源:oschina作者:Idea人点击

分享

一、基本概念

事务的原子性(Atomicity) 事务的原子性指的是,事务中包含的程序作为数据库的逻辑工作单位,它所做的对数据改操作要全部执行,要么全部不执行。这种特性称为原子性。 事务的原子性要求,如果把一个事务看作是一个程序,它要么完整的被执行,要么完全执行。就是说事务的操纵序列或者完全应用到数据库或者完全不影响数据库。这种特性称为原子性 假如用户在一个事务内完成了对数据库的更新,这时所有的更新对外部世界必须是可见的,或者完全没有更新。前者称事务已提交,后者称事务撤销。DBMS必须确保由成功提交的事物完成的所有操作在数据库内有完全的反映,而失败的事务对数据库完全没有影响


-------------------------------------------------------------------------------------------------------------


事务的一致性(Consistency) 指在一个事务执行之前和执行之后数据库都必须处于一致性状态。这种特性称为事务的一致性。假如数据库的状态满足所有的完整性约束,就说该数据库是一致的。(打个比方,如果从 A 账户转账到 B 账户,不可能因为 A 账户扣了钱,而 B 账户没有加钱吧)一致性处理数据库中对所有语义约束的保护。假如数据库的状态满足所有的完整性约束,就说该数据库是一致的。例如,当数据库处于一致性状态S1时,对数据库执行一 个事务,在事务执行期间假定数据库的状态是不一致的,当事务执行结束时,数据库处在一致性状态S2


-------------------------------------------------------------------------------------------------------------


隔离性(Isolation) 隔离性指并发的事务是相互隔离的。即一个事务内部的操作及正在操作的数据必须封锁起来,不被企图进行修改的事务看到 分离性是DBMS针对并发事务间的冲突提供的安全保证。DBMS可以通过加锁在并发执行的事务间提供不同级别的分离。假如并发交叉执行的事务没有任何控制。操纵相同的共享对象的多个并发事务的执行可能引起异常情况(打个比方,当我们编写了一条 update 语句,提交到数据库的一刹那间,有可能别人也提交了一条 delete 语句到数据库中。也许我们都是对同一条记录进行操作,可以想象,如果不稍加控制,就会出大麻烦来。我们必须保证数据库操作之间是“隔离”的(线程之间有时也要做到隔离),彼此之间没有任何干扰。这就是:隔离性(Isolation)。)DBMS可以在并发执行的事务间提供不同级别的分离。分离的级别和并发事务的吞吐量之间存在反比关系。较多事务的可分离性可能会带来较高的冲突和较多的事务流产。流产的事务要消耗资源,这些资源必须要重新被访问。因此,确保高分离级别的DBMS需要更多的开销


要想真正的做到操作之间完全没有任何干扰是很难的,于是乎要制定一个规范。


1.READ_UNCOMMITTED


2.READ_COMMITTED


3.REPEATABLE_READ


4.SERIALIZABLE


千万不要去翻译,那只是一个代号而已。从上往下,级别越来越高,并发性越来越差,安全性越来越高,反之则反。


-------------------------------------------------------------------------------------------------------------


持久性(Durability) 持久性意味着当系统或介质发生故障时,确保已提交事务的更新不能丢失。(打个比方,当我们执行一条 insert 语句后,数据库必须要保证有一条数据永久地存放在磁盘中,这个也算事务的一条特性,它就是:持久性)即一旦一个事务提交,DBMS保证它对数据库数据的改变应该是永久性的,耐得住任何系统故障 。持久性通过数据库备份和恢复来保证 持久性意味着当系统或介质发生故障时,确保已提交事务的更新不能丢失。即对已提交事务的更新恢复。一旦一个事务被提交,DBMS必须保证提供适当的冗余,使其耐的住系统故障。所以,持久性主要在于DBMS的恢复性能


-------------------------------------------------------------------------------------------------------------


二、三类数据读问题和两类数据更新问题


1.Dirty Read(脏读)



余额应该为 1100 元才对!请看 T6 时间点,事务 A 此时查询余额为 900 元,这个数据就是脏数据,它是事务 A 造成的,明显事务没有进行隔离,渗过来了,乱套了。


2.Unrepeatable Read(不可重复读)



事务 A 其实除了查询了两次以外,其他什么事情都没有做,结果钱就从 1000 变成 0 了,这就是重复读了。可想而知,这是别人干的,不是我干的。其实这样也是合理的,毕竟事务 B 提交了事务,数据库将结果进行了持久化,所以事务 A 再次读取自然就发生了变化。


这种现象基本上是可以理解的,但在有些变态的场景下却是不允许的。毕竟这种现象也是事务之间没有隔离所造成的,但我们对于这种问题,似乎可以忽略。


3.Phantom Read(幻读)



银行工作人员,每次统计总存款,都看到不一样的结果。不过这也确实也挺正常的,总存款增多了,肯定是这个时候有人在存钱。但是如果银行系统真的这样设计,那算是玩完了。这同样也是事务没有隔离所造成的,但对于大多数应用系统而言,这似乎也是正常的,可以理解,也是允许的。银行里那些恶心的那些系统,要求非常严密,统计的时候,甚至会将所有的其他操作给隔离开,这种隔离级别就算非常高了(估计要到 SERIALIZABLE 级别了)。


第一类丢失更新,A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来:



但是,在当前的四种任意隔离级别中,都不会发生该情况,不然绝对乱套,我都没提交事务只是撤销,就把别人的给覆盖了,这也太恐怖了。


第二类丢失更新,B事务覆盖A事务已经提交的数据,造成A事务所做操作丢失



归纳一下,以上提到了事务并发所引起的跟读取数据有关的问题,各用一句话来描述一下:


1.脏读:事务 A 读取了事务 B 未提交的数据,并在这个基础上又做了其他操作。


2.不可重复读:事务 A 读取了事务 B已提交的更改数据。


3.幻读:事务 A 读取了事务 B 已提交的新增数据。


第一条是坚决抵制的,后两条在大多数情况下可不作考虑。


这就是为什么必须要有事务隔离级别这个东西了,它就像一面墙一样,隔离不同的事务。看下面这个表格,您就清楚了不同的事务隔离级别能处理怎样的事务并发问题:



根据您的实际需求,再参考这张表,最后确定事务隔离级别,应该不再是一件难事了。

三、JDBC事务

JDBC 也提供了这四类事务隔离级别,但默认事务隔离级别对不同数据库产品而言,却是不一样的。我们熟知的MySQL数据库的默认事务隔离级别就是READ_COMMITTED,Oracle、SQL Server、DB2等都有有自己的默认值。我认为 READ_COMMITTED 已经可以解决绝大多数问题了,其他的就具体情况具体分析吧。


提示:在Java.sql.Connection 类中可查看所有的隔离级别。


我们知道 JDBC 只是连接 Java 程序与数据库的桥梁而已,那么数据库又是怎样隔离事务的呢?其实它就是“锁”这个东西。当插入数据时,就锁定表,这叫“锁表”;当更新数据时,就锁定行,这叫“锁行”。当然这个已经超出了我们今天讨论的范围,所以还是留点空间给我们的 DBA 同学吧,免得他没啥好写的了。

四、Spring事务管理

除了 JDBC 给我们提供的事务隔离级别这种解决方案以外,还有哪些解决方案可以完善事务管理功能呢?


不妨看看spring的解决方案吧,其实它是对 JDBC 的一个补充或扩展。它提供了一个非常重要的功能,就是:事务传播行为(TransactionPropagation Behavior)。


确实够牛逼的,Spring 一下子就提供了 7 种事务传播行为,这 7 种行为一出现,真的是亮瞎了!


1.PROPAGATION_REQUIRED


2.RROPAGATION_REQUIRES_NEW


3.PROPAGATION_NESTED


4.PROPAGATION_SUPPORTS


5.PROPAGATION_NOT_SUPPORTED


6.PROPAGATION_NEVER


7.PROPAGATION_MANDATORY


看了Spring 参考手册之后,更是晕了,这到底是在干嘛?


首先要明确的是,事务是从哪里来?传播到哪里去?答案是,从方法 A 传播到方法 B。Spring 解决的只是方法之间的事务传播,那情况就多了,比如:

方法 A 有事务,方法 B 也有事务。
方法 A 有事务,方法 B 没有事务。
方法 A 没有事务,方法 B 有事务。
方法 A 没有事务,方法 B 也没有事务。

这样就是 4 种了,还有 3 种特殊情况。还是用我的 Style 给大家做一个分析吧:


@Transactional
void A(){
}
@Transactional
void B(){
A();
}

假设事务从方法 A 传播到方法 B,您需要面对方法 B,问自己一个问题:


方法 A 有事务吗?

如果没有,就开启一个事务;如果有,就加入当前事务(方法A加入到方法B)。这就是PROPAGATION_REQUIRED,它也是 Spring 提供的默认事务传播行为,适合绝大多数情况。
如果没有,就开启一个事务;如果有,就将当前事务挂起。这就是RROPAGATION_REQUIRES_NEW,意思就是创建了一个新事务,它和原来的事务没有任何关系了。
如果没有,就开启一个事务;如果有,就在当前事务中嵌套其他事务。这就是PROPAGATION_NESTED,也就是传说中的“嵌套事务”了,所嵌套的子事务与主事务之间是有关联的(当主事务提交或回滚,子事务也会提交或回滚)。
如果没有,就以非事务方式执行;如果有,就使用当前事务。这就是PROPAGATION_SUPPORTS,这种方式非常随意,没有就没有,有就有,有点无所谓的态度,反正我是支持你的。
如果没有,就以非事务方式执行;如果有,就将当前事务挂起。这就是PROPAGATION_NOT_SUPPORTED,这种方式非常强硬,没有就没有,有我也不支持你,把你挂起来,不鸟你。
如果没有,就以非事务方式执行;如果有,就抛出异常。这就是PROPAGATION_NEVER,这种方式更猛,没有就没有,有了反而报错,确实够牛的,它说:我从不支持事务!
如果没有,就抛出异常;如果有,就使用当前事务。这就是PROPAGATION_MANDATORY,这种方式可以说是牛逼中的牛逼了,没有事务直接就报错,确实够狠的,它说:我必须要有事务!

看到我上面这段解释,小伙伴们是否已经感受到,被打通任督二脉的感觉?多读几遍,体会一下,就是您自己的东西了。


需要注意的是PROPAGATION_NESTED,不要被它的名字所欺骗,Nested(嵌套),所以凡是在类似方法 A 调用方法 B 的时候,在方法 B 上使用了这种事务传播行为,如果您真的那样做了,那您就错了。因为您错误地以为PROPAGATION_NESTED 就是为方法嵌套调用而准备的,其实默认的PROPAGATION_REQUIRED 就可以帮助您,做您想要做的事情了。


Spring 给我们带来了事务传播行为,这确实是一个非常强大而又实用的功能。除此以外,也提供了一些小的附加功能,比如:


1.事务超时(Transaction Timeout):为了解决事务时间太长,消耗太多的资源,所以故意给事务设置一个最大时常,如果超过了,就回滚事务。


2.只事务(Readonly Transaction):为了忽略那些不需要事务的方法,比如读取数据,这样可以有效地提高一些性能。


最后,推荐大家使用 Spring 的注解式事务配置,而放弃 XML 式事务配置。因为注解实在是太优雅了,当然这一切都取决于您自身的情况了。


在 Spring 配置文件中使用:



在需要事务的方法上使用:


@Transactional
public void xxx() {
...
}

可在@Transactional 注解中设置:事务隔离级别、事务传播行为、事务超时时间、是否只读事务。

五、思维导向图

参考资料


https://www.cnblogs.com/lc-ant/p/3981834.html


http://blog.csdn.net/u014079773/article/details/52808193


http://blog.csdn.net/lovesomnus/article/details/44459675


最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台