一 事务
事务的特性
事务隔离级别
二 Spring事务
Spring 编程式事务
使用方式
Spring 声明式事务
使用方式
Spring中的事务隔离级别
Spring事务的传播方式
Spring 事务实现原理
Spring 事务注意的问题
事务失效问题
(如果觉得文章写的不错的话,可以关注我的个人账号)
一、事务
事务是指 用户在执行其定义的一个数据库操作序列时,操作序列作为一个不可分割的工作单位,这些操作的执行要么全部都生效要么全部都不生效。
事务的开始和结束可以由用户显式控制。如果用户没有显式定义事务,则按照数据库缺省规定自动划分事务。
在SQL中,事务相关的语句有3条:
BEGIN TRANSACTION
COMMIT
ROLLBACK
- BEGIN TRANSACTION 表示事务开始,一个事务开始后,必须以COMMIT或ROLLBACK结束。
- COMMIT 表示提交,即提交事务的所有操作。具体的说就是将事务中所有对数据库的更新写回到磁盘上的物理数据库中去,事务正常结束。
- ROLLBACK 表示回滚,即在事务运行的过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已完成的操作全部撤销,回滚到事务开始时的状态。
事务的特性
事务具有四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持续性(Durability)。这四个特性简称为ACID特性(ACID properties)。
- 原子性
事务是数据库的逻辑工作单位,事务中包括的所有操作要么都做,要么都不做。 - 一致性
事务执行的结果必须是使数据库从一个一致性状态变成另一个一致性状态。因此当数据库指包含成功事务提供的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分一写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。例如,在银行中有A、B两个账号,现在公司想从账号A中取出一万元,存入账号B中。那么就可以定义一个事务,改事务包括两个操作,第一个操作是从账号A中减去一万元,第二个操作是向账号B中加入一万元。这两个操作要么全做,要么全不做。全做或者全不做,数据库都处于一致性状态。如果只做一个操作则用户逻辑上就会发生错误,少了一万元,这时数据库就出现不一致性状态。可见一致性与原子性密切相关。 - 隔离性
一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能相互干扰。 - 持续性
持续性也称永久性(Permanence),指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其执行结果有任何影响。
事务是恢复和并发控制的基本单位。保证事务ACID特性是事务管理的重要任务。事务ACID特性可能遭到破坏的因素有:
(1) 多个事务并行运行时,不同事务的操作交叉执行;
(2) 事务在运行过程中被强行终止。
在第一种情况下,数据库管理系统必须保证多个事务的交叉运行不影响这些事务的原子性。在第二种情况下,数据库管理系统必须保证被强行终止的事务对数据库和其他事务没有任何影响。
这些就是数据库管理系统中恢复机制和并发控制机制的责任。
事务隔离级别
在许多事务处理同一个数据时,特别是并发处理数据时,如果没有采取有效的隔离机制,就会带来一些问题。比如出现脏读、不可重复读、幻读等异常情况。
- 脏读:一个事务读取到另一个事务未提交的更新数据。
- 不可重复读:一个事务两次读取同一行的数据,结果得到不同状态的结果,中间正好另一个事务更新或者删除了该数据,两次结果相异,不可被信任。
- 幻读也叫虚读:一个事务执行两次查询,第二次结果集包含第一次中没有 的数据行,造成两次结果不一致,这是另一个事务在这两次查询中间插入了数据造成的。
非重复度和幻像读的区别
非重复读是指同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。(A transaction rereads data it has previously read and finds that another committed transaction has modified or deleted the data. )
幻像读是指同一查询在同一事务中多次进行,由于其他提交事务所做的插入操作,每次返回不同的结果集,此时发生幻像读。(A transaction reexecutes a query returning a set of rows that satisfies a search condition and finds that another committed transaction has inserted additional rows that satisfy the condition. )
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复 读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
表面上看,区别就在于非重复读能看见其他事务提交的修改和删除,而幻像能看见其他事务提交的插入。
由于对事务的并发控制需要耗费性能,越准确的控制往往意味着耗费越多的性能。为了实现效率的最大化,通常在一些业务情况下我们允许一些异常情况发生(一般不影响最终结果)。根据异常发生的可能性和效率的差异,我们将事务控制划分为不同的级别,即事务的隔离级别。
事务的隔离级别定义了不同级别下可能发生的一些异常情况。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(Read uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable ) | 不可能 | 不可能 | 不可能 |
未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读。
Mysql默认隔离级别为可重复读。其中,InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了幻象读问题。
串行化(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。在这个级别,可能导致大量的超时现象和锁竞争
二、Spring事务
Spring对事务提供了两种支持方式:编程式事务 和 声明式事务。编程式事务是指通过在编程时直接在业务逻辑代码中写入事务控制的相关代码,与业务逻辑代码耦合,一般不建议使用这种方式;声明式事务则通过AOP的方式来进行事务控制,对事务的控制逻辑不会侵入到业务逻辑代码中。
Spring 编程式事务
使用方式
public void transactionTest() {
Connection conn = null;
UserTransaction tx = null;
try {
tx = getUserTransaction(); //1.获取事务
tx.begin(); //2.开启JTA事务
conn = getDataSource().getConnection(); //3.获取JDBC
String sql = "select * from table"; //4.声明SQL
PreparedStatement pstmt = conn.prepareStatement(sql);//5.预编译SQL
ResultSet rs = pstmt.executeQuery(); //6.执行SQL
process(rs); //7.处理结果集
closeResultSet(rs); //8.释放结果集
tx.commit(); //9.提交事务
} catch (Exception e) {
tx.rollback(); //10.回滚事务
throw e;
} finally {
close(conn); //11.关闭连接
}
}
使用template
//Spring 编程式事务
public void transactionTemplateTest() {
TransactionTemplate transactionTemplate = new TransactionTemplate(txManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); //设置事务隔离级别
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);//设置为required传播方式
...
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
//事务控制逻辑
jdbcTemplate.update(SQL, args);
...
}});
}
Spring 声明式事务
使用方式
声明式事务支持两种使用方式:
- 在配置文件(xml)中声明相关事务规则
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
default-autowire="byName"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
<!-- 引入db.properties文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置连接池(数据源) -->
<bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
<property name="driverClass" value="${driverClass}"></property>
<property name="jdbcUrl" value="${jdbcUrl}"></property>
<property name="user" value="${user}"></property>
<property name="password" value="${password}"></property>
<property name="minPoolSize" value="${minPoolSize}"></property>
<property name="maxPoolSize" value="${maxPoolSize}"></property>
<property name="initialPoolSize" value="${initialPoolSize}"></property>
</bean>
<!-- 配置JdbcTemplate -->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="j">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务管理器,事务核心管理器,封装了所有事务操作。 关联连接池 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 配置方法的事务属性,支持通配符 isolation:隔离级别 propagation:传播行为 read-only:是否只读 -->
<tx:method name="save*" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="false" />
<tx:method name="persist*" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="false" />
<tx:method name="update*" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="false" />
<tx:method name="modify*" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="false" />
<tx:method name="delete*" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="false" />
<tx:method name="remove*" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="false" />
<tx:method name="get*" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="true" />
<tx:method name="find*" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="true" />
<tx:method name="transfer" isolation="REPEATABLE_READ"
propagation="REQUIRED" read-only="false" />
</tx:attributes>
</tx:advice>
<!-- 配合事务切入点 -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut expression="execution(* com.test.service.imp.*.*(..))" id="pointcut"/>
<!-- 配置切面 : 通知+切点 advice-ref:通知的名称 pointcut-ref:切点的名称 -->
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>
<!--开启注解的方式-->
<tx:annotation-driven transaction-manager="transactioManager" />
</beans>
2.使用@Transactional 注解声明事务规则
@Transactional
public void transactionTest() {
//事务内逻辑
jdbcTemplate.update(SQL, args);
...
}
@Transactional可指定的事务属性
@isolation:用于指定事务的隔离级别。默认为底层事务的隔离级别
@noRollbackFor:指定遇到特定异常时不回滚事务
@noRollbackForClassName:指定遇到特定的多个异常时不回滚事务
@propagation:指定事务传播行为
@readOnly:指定事务是否可读
@rollbackFor:指定遇到特定异常时回滚事务
@rollbackForClassName:指定遇到特定的多个异常时回滚事务
@timeout:指定事务的超长时长。
Spring中的事务隔离级别
事务隔离级别:用来解决并发事务时出现的问题,其使用TransactionDefinition中的静态变量来指定。Spring中支持五种事务隔离级别方式:
- ISOLATION_DEFAULT:默认隔离级别,即使用底层数据库默认的隔离级别;
- ISOLATION_READ_UNCOMMITTED:未提交读;
- ISOLATION_READ_COMMITTED:提交读,一般情况下我们使用这个;
- ISOLATION_REPEATABLE_READ:可重复读;
- ISOLATION_SERIALIZABLE:序列化。
Spring事务的传播方式
事务分为物理事务和逻辑事务:
物理事务:就是底层数据库提供的事务支持,如JDBC或JTA提供的事务;
逻辑事务:是Spring管理的事务,不同于物理事务,逻辑事务提供更丰富的控制,而且如果想得到Spring事务管理的好处,必须使用逻辑事务,因此在Spring中如果没特别强调一般就是逻辑事务。
Spring管理的事务是逻辑事务,物理事务和逻辑事务最大差别就在于事务传播行为,事务传播行为用于指定在多个事务方法间调用时,事务是如何在这些方法间传播的,Spring共支持7种传播行为:
- PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。是Spring默认的传播行为。
- PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价PROPAGATION_REQUIRED。嵌套事务使用数据库中的保存点来实现,即嵌套事务回滚不影响外部事务,但外部事务回滚将导致嵌套事务回滚。
Spring 事务实现原理
在应用系统调用 事务声明 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据事务配置信息,这个代理对象决定该声明事务 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务。
Spring AOP 代理有 CglibAopProxy 和 JdkDynamicAopProxy 两种,以CglibAopProxy 为例,对于CglibAopProxy,需要调用其内部类的DynamicAdvisedInterceptor 的 intercept 方法。对于 JdkDynamicAopProxy,则调用其 invoke 方法。
Spring事务中需要注意的问题
事务失效问题
在同一个代理对象内部,事务方法之间的直接嵌套调用,普通方法和事务方法之间的直接嵌套调用,都会造成事务异常!具体表现为某些传播行为不生效或者直接事务控制不生效。
@Service
public class DemoService {
@Transaction
public void transactionMethod1()
{
op1();
op2();
...
}
public void commonMethod()
{
...
transactionMethod1(); //照顾基础不牢的朋友,这里相当于 this.transactionMethod1();
...
}
@Transaction
public void transactionMethod2()
{
...
this.transactionMethod3();
...
}
@Transaction(propagation= Propagation.REQUIRES_NEW)
public void transactionMethod3()
{
op3();
...
}
}
上面代码中,如果调用 DemoService 的 bean 对象的commonMethod ,则transactionMethod1里定义的事务将不生效(比如op2发生错误时,并不会回滚op1的操作),bean 调用 transactionMethod2时,transactionMethod2时里面调用的transactionMethod3也不会开启新的事务。
为什么会这样?
上面的实现机制中讲到,AOP的实现都是通过动态代理来实现,而AOP限制了我们只能在目标方法的开始和结束作为切点做切入处理增强。当动态代理对象最终调用的原始对象的目标方法时,并不能干预到目标方法内的方法调用行为,如果原始对象直接调用(用this.xxx方式)其他同类方法时,实际调用的是原始对象自身的方法,而不是代理类对象增强后(增加事务控制后)的方法。此时Spring对方法事务的控制(包括事务的传播行为、事务的隔离级别等)完全失效。
如何解决?
要想解决此类问题,主要都在于原始对象在调用对象内其他方法时,不要使用this.xxx的方式直接调用,通过注入或者获取代理对象的方式,使用代理对象调用需要调用的方法。下面列举几个解决方式:
- 1.注入自身,使用代理对象调用
@Service
public class DemoService {
@Autowired
DemoService demoService;
@Transaction
public void transactionMethod1()
{
op1();
op2();
...
}
public void commonMethod()
{
...
//this.transactionMethod1() -> demoService.transactionMethod1()
demoService.transactionMethod1();
...
}
...
}
- 2.使用AopContext,获取当前代理对象
@Service
public class DemoService {
@Transaction
public void transactionMethod1()
{
op1();
op2();
...
}
public void commonMethod()
{
...
//this.transactionMethod1() -> ((DemoService)AopContext.currentProxy()).transactionMethod1();)
((DemoService)AopContext.currentProxy()).transactionMethod1();
...
}
...
}
- 3.使用BeanFactory获取代理对象(代码略)
如果觉得文章写得不错的话,欢迎关注个人公众号~