一、简介
一个数据库事务是一个被视为单一的工作单元的操作序列。这些操作应该要么完整地执行,要么完全不执行,以确保数据完整性和一致性。事务的概念可以描述为具有以下四个关键属性说成是 ACID:
-
原子性:事务应该当作一个单独单元的操作,这意味着整个序列操作要么是成功,要么是失败的。
-
一致性:事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态
-
隔离性:可能同时处理很多有相同的数据集的事务,每个事务应该与其他事务隔离,以防止数据损坏。
- 持久性:一个事务一旦完成全部操作后,这个事务的结果必须是永久性的,不能因系统故障而从数据库中删除。
Spring 支持两种类型的事务管理:
- 编程式事务管理 :这意味着你在编程的帮助下有管理事务。这给了你极大的灵活性,但却很难维护。
- 声明式事务管理 :这意味着你从业务代码中分离事务管理。你仅仅使用注释或 XML 配置来管理事务。
Spring 事务抽象的关键是由 org.springframework.transaction.PlatformTransactionManager 接口定义,如下所示:
public interface PlatformTransactionManager {
//根据指定的传播行为,该方法返回当前活动事务或创建一个新的事务。
TransactionStatus getTransaction(TransactionDefinition var1) throws TransactionException;
//该方法提交给定的事务和关于它的状态。
void commit(TransactionStatus var1) throws TransactionException;
//该方法执行一个给定事务的回滚。
void rollback(TransactionStatus var1) throws TransactionException;
}
TransactionDefinition 是在 Spring 中事务支持的核心接口,它的定义如下:
public interface TransactionDefinition {
//该方法返回传播行为。Spring 提供了与 EJB CMT 类似的所有的事务传播选项。
int getPropagationBehavior();
//该方法返回该事务独立于其他事务的工作的程度。
int getIsolationLevel();
//该方法返回该事务的名称。
String getName();
//该方法返回以秒为单位的时间间隔,事务必须在该时间间隔内完成。
int getTimeout();
//该方法返回该事务是否是只读的。
boolean isReadOnly();
}
二、 编程式事务实现:
<!-- Initialization for TransactionManager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
@Autowired
private PlatformTransactionManager transactionManager;
@Override
public boolean addBacth(List<Book> books) {
//使用默认的传播行为
TransactionDefinition def = new DefaultTransactionDefinition();
//获得当前活动事务或创建一个新的事务
TransactionStatus status = transactionManager.getTransaction(def);
try {
// do something
//提交事务
transactionManager.commit(status);
} catch (Exception e) {
//事务回滚
transactionManager.rollback(status);
return false;
}
return true;
}
三、声明式事务实现
<!-- Initialization for TransactionManager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="createOperation"
expression="execution(* com.spring.learn.service.impl.BookService2Impl.addBacth(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="createOperation"/>
</aop:config>
// 使用声明式事务 业务逻辑中没有侵入式的代码
@Override
public boolean addBacth(List<Book> books) {
// do something
return true;
}
还可使用注解实现事务,配置文件更加简化
<!-- Initialization for TransactionManager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
//加上Transactional注解即可
@Override
@Transactional(rollbackFor = Exception.class)
public boolean addBacth(List<Book> books) {
// do something
return true;
}
四、spring 事务的隔离级别
spring提供了5种隔离级别,要弄懂隔离级别就要先了解事务在并发环境下会面临的几种问题:
脏读:在一个事务处理过程里读取了另一个未提交的事务中的数据。事务A读取了事务B中尚未提交的数据。如果事务B回滚,则A读取使用了错误的数据。
时间顺序 | A(取钱,卡中余额0元) | B(转账,卡中余额100元) |
T1 | 开始事务 | |
T2 | 向A转账100元,扣除100 | |
开始事务 | ||
T3 | 查询余额:100元 | |
T4 | 取款100元 | |
提交事务 | ||
T5 | 发生错误,事务回滚 | |
T6 | 查询余额:100元 | |
备注 | 结果A取走了100元,而B的钱一分没少,A读取了事务B中尚未提交的数据。 |
不可重复读:读取了提交的新事物(指更新操作)。对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务提交修改了。
不可重复读和脏读的区别是:脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如同一个事物前后两次查询同一个数据,期望两次读的内容是一样的,但是因为读的过程中,因为令一个数据写了该数据,导致不可重复读。
时间顺序 | A | B |
T1 | 开始事务 | |
T2 | 第一次查询额:100 | |
T3 | 开始事务 | |
T4 | 其它操作 | |
T5 | 更改余额为:200 | |
T6 | 提交事务 | |
T7 | 第二次查询额:200 | |
备注 | A期望两次查到一样的数据 |
幻读:读取了提交的新事物(指增删操作),幻读也叫虚读。
幻读是事务非独立执行时发生的一种现象。幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据,而幻读针对的是一批数据整体(比如数据的个数)。
第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
时间顺序 | A | B |
T1 | 开始事务 | |
T2 | 第一次查询有100条数据 | |
T3 | 开始事务 | |
T4 | 其它操作 | |
T5 | 删除50条数据 | |
T6 | 提交事务 | |
T7 | 第二次查询有50条数据 | |
备注 | A两次查询到的数据量应该一致 |
现在回过头来看spring提供的五种隔离级别:
- TransactionDefinition.ISOLATION_DEFAULT:默认的隔离级别,使用数据库默认的事务隔离级别.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED:读未提交,在这种隔离级别下,所有事务能够读取其他事务未提交的数据。读取其他事务未提交的数据,会造成脏读。因此在该种隔离级别下,不能解决脏读、不可重复读和幻读。
- TransactionDefinition.ISOLATION_READ_COMMITTED:读已提交,在这种隔离级别下,所有事务只能读取其他事务已经提交的内容。能够彻底解决脏读的现象。但在这种隔离级别下,会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象,也就是出现了不可重复读。
这是大多数数据库系统默认的隔离级别,例如Oracle和SQL Server,但mysql不是。
- TransactionDefinition.ISOLATION_REPEATABLE_READ:可重复读,在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。也就是某个事务在执行的过程中,不允许其他事务进行update操作,但允许其他事务进行add操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读。
这是mysql的默认事务隔离级别
- TransactionDefinition.ISOLATION_SERIALIZABLE:串行化,在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。但是安全和效率不能兼得,这样事务隔离级别,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。
下面用一张表格来表示不同的隔离级别能解决的问题:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交 | × | × | × |
读已提交 | √ | × | × |
可重复读 | √ | √ | × |
串行化 | √ | √ | √ |
五、spring事务的传播行为
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。
Spring定义了七种传播行为:
传播行为 | 含义 |
PROPAGATION_REQUIRED | 支持当前事务;如果不存在事务,则创建一个新的事务。 |
PROPAGATION_SUPPORTS | 支持当前事务;如果不存在,则执行非事务性。 |
PROPAGATION_MANDATORY | 支持当前事务;如果不存在当前事务,则抛出一个异常。 |
PROPAGATION_REQUIRES_NEW | 创建一个新事务,如果存在一个事务,则把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 不支持当前事务;而总是执行非事务性。 |
PROPAGATION_NEVER | 不支持当前事务;如果存在当前事务,则抛出一个异常。 |
PROPAGATION_NESTED | 如果存在当前事务,则在一个嵌套的事务中执行。 |
1.PROPAGATION_REQUIRED
如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
// do something
}
单独调用methodB时,因为当前上下文不存在事务,所以会开启一个新的事务。调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务A,当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务A中来。
2.PROPAGATION_SUPPORTS
如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
// do something
}
单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。
3.PROPAGATION_MANDATORY
如果已经存在一个事务,加入当前事务。如果没有一个活动的事务,则抛出异常。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
// do something
}
当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常:
当调用methodA时,methodB则加入到methodA的事务中。
4.PROPAGATION_REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// do something
}
单独调用method时会创建一个事务,当调用methodA时会创建一个事务A,当执行到methodB时会把事务A挂起,然后创建事务B,事务B提交后,再继续执行事务A。内层事务B与外层事务A是两个独立的事务,互不相干。当执行methodB后面的代码出错时,事务A回滚并不会影响到事务B已提交的内容。
5.PROPAGATION_NOT_SUPPORTED
总是非事务地执行,并挂起任何存在的事务。
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
// do something
}
// 事务属性为NOT_SUPPORTED
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodB() {
// do something
}
当调用methodA时会创建一个事务A,当执行到methodB时会把事务A挂起,然后以非事务的方式执行methodB。
6.PROPAGATION_NEVER
总是非事务地执行,如果存在一个活动事务,则抛出异常。
7.PROPAGATION_NESTED
如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按PROPAGATION_REQUIRED 属性执行。 这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。 需要JDBC 驱动的java.sql.Savepoint类。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true(属性值默认为false)。
@Transactional(propagation = Propagation.REQUIRED)
methodA(){
methodB();
//do something
}
@Transactional(propagation = Propagation.NEWSTED)
methodB(){
//do something
}
如果单独调用methodB方法,则按PROPAGATION_REQUIRED 属性执行。调用methodA时,在methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码调用失败,则回滚包括methodB方法的所有操作。
嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。