事务之一:数据库事务基础知识回顾
一、什么是事务?
- 事务是一种机制,把成组的操作视为一个操作进行处理
- 在事务处理中,要么所有的操作都执行,要么所有的操作都不执行
- 在事务中涉及的操作可能依赖于很多不同的数据库和服务器事务处理的ACID属性
1.1、ACID
事务是数据库运行中的一个逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。事务具有ACID四种特性:
原子性(Atomic)(Atomicity)
事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
一致性(Consistent)(Consistency)
事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。
隔离性(Insulation)(Isolation)
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为隔离性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
持久性(Duration)(Durability)
事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
1.2、事务模型
最常见的两种事务模型:平面(Flat)事务和嵌入式(Nested)事务。 平面事务是由一系列的原子性的操作构成,这些操作一起组成了单个工作单元。而嵌入式事务容许将原子性的工作单元嵌入到其它的工作单元中,并且对于嵌入式事务来说,嵌入的子事务即使回滚了,也不会引起外层事务的回滚。但是,如果嵌入式事务最终不能够提交的话,整个事务也将失败。可以将嵌入式事务理解成事务 树,它们存在单个根或顶级(top-level)事务,根事务是主事务。
其它事务模型还有Chained事务、Sagas事务。
当前的EJB规范没有对嵌入式事务做出具体的要求,因此EJB的事务管理器只支持平面事务。
二、数据库事务的四种隔离级别如下:
1.read uncommittted(读取未提交的内容)--(解决丢失更新,会出现脏读、不可重复读、幻读)
在read uncommitted隔离级别,所有事务都可以“看到”未提交的事务的执行结果。在这种级别上,可能会产生很多问题,除非用户明确自己在做什么,并且有很好的理由选择这样做。读取未提交数据,也称之为“脏读”。
2.read committed(读取提交内容)--(解决丢失更新、脏读,会出现不可重复读、幻读)
大多数数据库系统的默认隔离级别是read committed(但不是MySQL默认的)。它满足了隔离的早先定义:一个事务在开始时,只能“看见”已经提交事务所做的改变,一个事务从开始到提交前,所做的任何数据改变都是不可见的,除非已经提交。这种隔离级别也支持所谓的“不可重复读”。这意味着用户运行同一语句两次,看到的结果是不同的。
3.repeatable read(可重读)--(解决丢失更新、脏读、不可重复读,会出现幻读)
repeatable read隔离级别解决了read uncommitted隔离导致的问题。它确保在同一事务的多个实例在并发读取数据时,会“看到同样的”数据行。不过这可能导致幻读,所谓幻读指当用户读取某一范围的数据行时,另一个事务又在修改该范围内插入新行,当用户再读取该范围的数据行时,会发现有新的“幻影”。“repeatable read”是mysql的默认事务隔离级别(如MySQL InnoDB存储引擎)。
4.serializable(可串行化)--(解决丢失更新、脏读、不可重复读、幻读)
serializable是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,serializable是在每个读的数据行上加锁。在这个级别,可能导致大量的超时现象和锁竞争的现象。
Isolation并发可能引起如下问题
第一类:丢失更新 :
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所做的更新,这将导致数据丢失。
例:
事务A和事务B同时修改某行的值,
1.事务A将数值改为1并提交
2.事务B将数值改为2并提交。
这时数据的值为2,事务A所做的更新将会丢失。
解决办法:对行加锁,只允许并发一个更新事务。
第二类:脏读:
一个事务读到另一个事务未提交的更新数据
例:
1.Mary的原工资为1000, 财务人员将Mary的工资改为了8000(但未提交事务)
2.Mary读取自己的工资 ,发现自己的工资变为了8000,欢天喜地!
3.而财务发现操作有误,回滚了事务,Mary的工资又变为了1000, 像这样,Mary记取的工资数8000是一个脏数据。
第三类:不可重复读:
在同一个事务中,多次读取同一数据,返回的结果有所不同. 换句话说就是,后续读取可以读到另一个事务已提交的更新数据. 相反"可重复读"在同一事务多次读取数据时,能够保证所读数据一样,也就是后续读取不能读到另一事务已提交的更新数据.
例:
1.在事务1中,Mary 读取了自己的工资为1000,操作并没有完成
2.在事务2中,这时财务人员修改了Mary的工资为2000,并提交了事务.
3.在事务1中,Mary 再次读取自己的工资时,工资变为了2000
解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。
第四类:幻读:
一个事务读取到另一个事务已提交的insert数据.
例:
第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时 (此时第一事务还未提交) ,第二个事务向表中插入一行新数据。这时第一个事务再去读取表时,发现表中还有没有修改的数据行,就好象发生了幻觉一样。
不可重复读和幻读的区别
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
上面说是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。
三、悲观锁和乐观锁
- 悲观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处 于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机 制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
- 乐观锁
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如 果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。
四、 事务类型(分类)
数据库事务类型有本地事务和分布式事务:
- 本地事务:就是普通事务,能保证单台数据库上的操作的ACID,被限定在一台数据库上;
- 分布式事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成的),分布式事务旨在保证这些本地事务的所有操作的ACID,使事务可以跨越多台数据库;
Java事务类型有JDBC事务和JTA事务:
- JDBC事务:就是数据库事务类型中的本地事务,通过Connection对象的控制来管理事务;
- JTA事务:JTA指Java事务API(Java Transaction API),是Java EE数据库事务规范, JTA只提供了事务管理接口,由应用程序服务器厂商(如WebSphere Application Server)提供实现,JTA事务比JDBC更强大,支持分布式事务。
Java EE事务类型有本地事务和全局事务:
- 本地事务:使用JDBC编程实现事务;
- 全局事务:由应用程序服务器提供,使用JTA事务;
按是否通过编程实现事务有声明式事务和编程式事务;
- 声明式事务: 通过注解或XML配置文件指定事务信息;
- 编程式事务:通过编写代码实现事务。
五、什么是分布式事务
- 在同一个事务中涉及到多个应用服务器。
- 在同一个事务中需要完成对多个RDBMS的更新操作。
- 需要在同一个事务中完成对数据库的更新,(从目的地)接收消息或发送JMS消息(到目的地)。
- 在同一个事务中要操作到多种资源,其中包括遗留系统、若干存储源(比如,数据库、消息Queue、其他遗留系统等)。
2、DTP
DTP:分布式事务处理( Distributed Transaction Processing , DTP )的概念。 Transaction ,即事务,又称之为交易,指一个程序或程序段,在一个或多个资源如数据库或文件上为完成某些功能的执行过程的集合。分布式事务处理是指一个事务可能涉及多个数据库操作,分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)。
3、X/Open DTP模型
X/Open DTP模型(1994):包括应用程序(AP)、事务管理器(TM)、资源管理器(RM)、通信资源管理器(CRM)四部分。在这个模型中,通常事务管理器(TM)是交易中间件,资源管理器(RM)是数据库,通信资源管理器(CRM)是消息中间件。
4、XA
XA:是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。XA之所以需要引入事务管理器是因为,在分布式系统中,两台机器理论上无 法达到一致的状态,需要引入一个单点进行协调。事务管理器控制着全局事务,管理事务生命周期,并协调资源。资源管理器负责控制和管理实际资源(如数据库或 JMS队列)。下图说明了事务管理器、资源管理器,与应用程序之间的关系:
1)事务管理器。事务管理器负责协调具体的资源管理器来完成事务控制。
2)资源管理器。资源管理器具体来说就是各种驱动程序,对于数据库来说,就是具体的JDBC驱动程序。
5、2PC
2PC:是指两阶段提交协议(Two-Phase Commit protocol)
所有关于分布式事务的介绍中都必然会讲到两阶段提交,因为它是实现XA分布式事务的关键(确切地说:两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做)。所谓的两个阶段是指:第一阶段:准备阶段和第二阶段:提交阶段。
如上图所示,分布式事务管理的角色一般都是有Transaction Manager (简称TM)来承担。其负责与Application Program(简称AP)、Resource Manager (简称RM) 以及其他的TM进行交互。这里RM所管理的Resource在分布式事务中涉及数据库、队列、文件、消息或其他在事务中被访问到的共享对象。TM一般负责处理基本的事务操作:Start、Commit和Abort。AP调用Start来启动事务的执行,调用 Commit来请求TM提交事务,调用Abort来请求TM中断事务操作。TM在分布式事务执行过程中会跟踪所有事务访问的RM。一旦事务准备提交,TM 就需要和事务涉及到的所有TM打交道来执行2PC.
- 准备阶段:事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回 失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。(关于每一个参与者在 准备阶段具体做了什么目前我还没有参考到确切的资料,但是有一点非常确定:参与者在准备阶段完成了几乎所有正式提交的动作,有的材料上说是进行了“试探性 的提交”,只保留了最后一步耗时非常短暂的正式提交操作给第二阶段执行。)
- 提交阶段:如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
在上面的两阶段提交中,存在着一个主事务管理器,称之为分布式协调者(distributed Transaction Coordinator,DTC)。运行中的DTC能够协调网络中的其他事务管理器,并控制事务的具体执行。下面是分布式2PC事务的据具体实现过程:
A)事务协调者发生准备提交(prepare to commit)消息给参与事务的各个事务管理器;
B)各个事务管理器可能会将接收到的消息传播给绑定到各个事务管理器的资源管理器;
C)各个事务管理器将运行结果报告给事务协调者。如果所有的事务管理器都希望执行提交操作,则需要将提交操作记录到日志中,以防止系统瘫痪的发生。
D)最后,事务协调者告知各个事务管理器去提交事务。事务管理器收到提交命令后,会依次调用各自资源管理器,从而完成资源的实际更新,并生效它。如果期间出现任何异常,借助日志能够执行恢复操作,从而实现事务的预期目标。
将提交分成两阶段进行的目的很明确,就是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作,这样,最后的提交阶段将是一个耗时极短的微小操作,这种操作在一个分布式系统中失败的概率是非常小的,也就是所谓的“网络通讯危险期”非常的短暂,这是两阶段提交确保分布式事务原子性的关键所在。 (唯一理论上两阶段提交出现问题的情况是当协调者发出提交指令后当机并出现磁盘故障等永久性错误,导致事务不可追踪和恢复)
从两阶段提交 的工作方式来看,很显然,在提交事务的过程中需要在多个节点之间进行协调,而各节点对锁资源的释放必须等到事务最终提交时,这样,比起一阶段提交,两阶段 提交在执行同样的事务时会消耗更多时间。事务执行时间的延长意味着锁资源发生冲突的概率增加,当事务的并发量达到一定数量的时候,就会出现大量事务积压甚至出现死锁,系统性能就会严重下滑。这就是使用XA事务。
6、XA驱动、XADataSource
如果计划用 JTA 界定事务,那么就需要有一个实现 javax.sql.XADataSource 、 javax.sql.XAConnection 和 javax.sql.XAResource 接口的 JDBC 驱动程序。一个实现了这些接口的驱动程序将可以参与 JTA 事务。一个 XADataSource 对象就是一个 XAConnection 对象的工厂。 XAConnection是参与 JTA 事务的 JDBC 连接。
XA连接区别于非XA连接。要记住的是XA连接是一个JTA事务中的参与者。这就意味着XA连接不支持JDBC的自动提交特性。 也就是说应用程序不必在XA连接上调用java.sql.Connection.commit()或 java.sql.Connection.rollback()。 相反,应用程序应该使用UserTransaction.begin()、UserTransaction.commit()和 UserTransaction.rollback()。
如下式weblogic中的源码片段。
JTSXAResourceImpl源码片段:
public int prepare(Xid xid)throws XAException { if(!jtsConn.getEnable2PC()) throw new XAException("JDBC driver does not support XA ... ); else return 0; } public void commit(Xid xid, boolean flag)throws XAException { ... jtsConn.internalCommit(); ... } public void rollback(Xid xid)throws XAException { ... jtsConn.internalRollback(); ... }
JTSConnection源码片段:
public synchronized void internalRollback() throws SQLException { ... connection.rollback(); ... internalClose(); ... } public void internalCommit() throws SQLException { ... connection.commit(); ... internalClose(); ... }