1. 事务
1.1 概念
事务就是逻辑上的一组操作,组成这组操作的各个单元,要么全都成功,要么全都失败。
1.2 特性
- 原子性:事务不可分割,整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像事务出来没有被执行过一样;
- 一致性:事务执行前后数据完整性保持一致;
- 隔离性:一个事务的执行不应该受到其他事务的干扰,指两个事务之间的隔离程度;
- 持久性:一旦事务结束,数据就持久化到数据库。
1.3 安全问题
如果不考虑事务的隔离性会导致读问题和写问题,以下是相关问题的介绍。
1.3.1 读问题
- 脏读:一个事务读到另一个事务未提交的数据;
- 不可重复读:一个事务读到另一个事务已经提交的update的数据,导致一个事务中多次查询结果不一致;
- 虚读(幻读):一个事务读到另一个事务已经提交的insert的数据,导致一个事务中多次查询结果不一致。
1.3.2 写问题
- 丢失更新:假设有一场景:张三和李四使用同一账户购买物品,账户的余额为10000元。张三刷卡消费了1000块,提交事务后账户余额变为9000元。与此同时,在张三未提交事务之前,李四查询到账户余额仍为10000元,李四网购消费了1000元,在张三提交事务后她再提交事务,此时会根据之前余额的10000元,扣减1000元,余额为9000元。由于是不同的事务,无法探知其他事物的操作,导致两者提交后,余额都为9000元。此时,张三的写的更新操作丢失了。
1.4 隔离级别
数据库规范中定义了事务之间的隔离级别,来解决读问题以及在不同程度上减少丢失更新的可能性。
- Read uncommitted:未提交读,任何读问题解决不了;
- Read committed:已提交读,解决脏读,但是不可重复读和虚读有可能发生;
- Repeatable read:重复读,解决脏读和不可重复读,但是虚读有可能发生。
- Serializable:序列化,解决所有读问题。
2. Spring的事务传播行为
传播行为是指方法之间的调用事务策略的问题。假设现在需要实现信用卡还款功能,有一个总的调用代码逻辑—RepaymentBatchService类的batch方法,它要实现的是记录还款功能成功的总卡数和对应完成的信息,而每一张卡的还款则是通过RepaymentService类的repay方法完成的。
如果只有一条事务,那么当调用RepaymentService的repay方法对某一张信用卡进行还款时发生了异常。如果将这条事务回滚,就会造成repay方法异常之前的所有操作都发生回滚,这是很糟糕的。我们可以通过另一种方法来处理,就是当batch方法调用repay方法时,会为repay方法创建一条新的事务,即使repay方法发生了异常,它也只会回滚自己的事务。类似这样一个方法调度另外一个方法时,可以对事务的特性进行传播配置的行为,称之为传播行为。
Spring中提供了七种事务的传播行为,是通过一个枚举类型去定义的,这个枚举类是org.springframework.transaction.annotation.Propagation,它定义了如下七种事务的传播行为:
- 保证多个操作在同一个事务中
- PROPAGATION_REQUIRED :默认值,当调用方法时,如果不存在当前事务,那么就创建事务;如果之前的方法已经存在事务了,那么就沿用之前的事务;
- PROPAGATION_SUPPORTS:支持事务,当方法调用时,如果不存在当前事务,那么不启用事务;如果存在当前事务,那么就沿用当前的事务;
- PROPAGATION_MANDATORY:方法必须在事务内运行,如果不存在当前事务,那么就抛出异常;
- 保证多个操作不在同一个事务中
- PROPAGATION_REQUIRES_NEW:无论是否存在当前事务,方法都会在新的事务中运行,也就是事务管理器会打开新的事务运行该方法;
- PROPAGATION_NOT_SUPPORTED:不支持事务,如果不存在当前事务也不会创建事务;如果存在当前事务,则挂起它,直至该方法结束后才恢复当前事务;适用于那些不需要事务的SQL;
- PROPAGATION_NEVER:不支持事务,只有在没有事务的环境才能运行它,如果方法存在当前事务,则抛出异常;
- 嵌套事务
- PROPAGATION_NESTED:嵌套事务,也就是调用方法如果抛出异常只回滚自己内部执行的SQL,而不回滚主方法的SQL。它的实现存在两种情况,如果当前数据库支持保存点,那么它就会在当前事务上使用保存点技术;如果发生异常则将方法内执行的SQL回滚到保存点上,而不是全部回滚,否则就等同于PROPAGATION_REQUIRES_NEW创建新的事务运行方法代码。
3. Spring事务管理API
3.1 PlatformTransactionManager:平台事务管理器
PlatformTransactionManager平台事务管理器是一个接口,是Spring用于管理事务的真正的对象。其有如下两个子类:
- DataSourceTransactionManager:底层使用JDBC管理事务。
- HibernateTransactionManager:底层使用Hibernate管理事务,整合Hibernate框架时会用到。
3.2 TransactionDefinition:事务定义信息
事务定义信息用于定义事务的相关的信息,隔离级别、超时信息、传播行为、是否只读。
3.3 TransactionStatus:事务的状态
事务状态用于记录在事务管理过程中,事务的状态的对象。
3.4 事务管理API的关系
Spring进行事务管理的时候,首先平台事务管理器根据事务定义信息进行事务的管理,在事务管理过程中,产生各种状态,将这些状态的信息记录到事务状态的对象中。
4. Spring事务管理
下面模拟一下两个账户之间的转账场景。
4.1 环境搭建
首先是创建数据库和数据库表,具体如下所示:
1
2
3
4
5
6
7CREATE DATABASE spring4;
USE spring4;
CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20),
money DOUBLE
);- 创建web项目和相关文件,并导入jar包,具体如下:
- 创建web项目和相关文件,并导入jar包,具体如下:
创建AccountDao接口和其实现类,代码如下:
1
2
3
4
5
6public interface AccountDao {
//转出
public void outMoney(String from, Double money);
//转进
public void inMoney(String to, Double money);
}
1 | public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao { |
注意:这里AccountDaoImpl 继承了JdbcDaoSupport类,我们可以通过this来获取Jdbc模板,而不需要我们手动注入。当然我们需要在配置文件给继承JdbcDaoSupport的类也就是AccountDaoImpl注入连接池才可以用Jdbc模板。具体参考JdbcDaoSupport类代码,下面给出核心部分代码:
4. 创建AccountServie接口和其实现类,代码如下:
1 | public interface AccountService { |
1 | public class AccountServiceImpl implements AccountService { |
在applicationContext.xml进行如下配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 引入数据库属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 配置C3P0连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 属性注入 -->
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置Dao -->
<bean id="accountDao" class="com.shoto.spring.txdemo.AccountDaoImpl">
<!-- 向Dao注入连接池,继承JdbcDaoSupport的类会自动创建jdbc模板 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置Service,并注入Dao -->
<bean id="accountService" class="com.shoto.spring.txdemo.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>
</beans>编写测试类,具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13(SpringJUnit4ClassRunner.class)
"classpath:applicationContext.xml") (
public class SpringDemo {
"accountService") (name=
private AccountService accountService;
//张三给李四转账1000元
public void test() {
accountService.transfer("张三", "李四", 1000d);
}
}
通过上面的代码容易知道,当outMoney方法与inMoney方法执行期间发生异常时,张三的余额会减少而李四的余额却不会对应的增加同等的金额。下面我们采用事务管理的方式来处理。
4.2 编程式事务(了解)
配置平台事务管理器
1
2
3
4
5<!-- 配置平台事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入连接池 -->
<property name="dataSource" ref="dataSource"/>
</bean>配置事务管理的模板类(由Spring提供)
1
2
3
4
5<!-- 配置事务管理模板 -->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<!-- 注入事务管理器 -->
<property name="transactionManager" ref="transactionManager"/>
</bean>在业务层注入事务管理的模板
首先在AccountServiceImpl类中添加如下代码:1
2
3
4
5
6private TransactionTemplate transactionTemplate;
//set方法注入事务管理模板
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
然后在applicationContext.xml进行如下配置:
4. 编写事务管理的代码,即修改AccountServiceImpl类的transfer方法如下:
1 |
|
- 运行测试,此时不会出现张三的余额会减少而李四的余额却不会对应的增加同等的金额的情况。
4.3 声明式事务管理(AOP方式)
4.3.1 XML方式
首先应该恢复转账环境,即恢复到未进行编程式事务配置管理之前的状态
引入aop的开发包
配置事务管理器
1
2
3
4
5<!-- 配置平台事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入连接池 -->
<property name="dataSource" ref="dataSource"/>
</bean>配置增强
1
2
3
4
5
6
7
8
9
10
11
12
13
14<!-- 配置事务的增强 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 下面是一些事务管理的规则,常用于实际开发中,save*表示以save开头的方法
propagation:表示传播行为
isolation:表示隔离级别
-->
<!-- <tx:method name="save*" propagation="REQUIRED" isolation="DEFAULT"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="find*" read-only="true"/> -->
<tx:method name="transfer" propagation="REQUIRED" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
注意:<tx:method>中的rollback-for=”Exception”表示发生异常时进行回滚操作,另外其还有一个属性no-rollback-for=”IndexOutOfBoundsException”表示在遇到某些异常如IndexOutOfBoundsException时不执行回滚操作。
5. AOP的配置
1 | <!-- aop配置 --> |
运行测试
4.3.2 注解方式(最简单)
首先应该恢复转账环境,即恢复到未进行编程式事务配置管理之前的状态
导入AOP相关jar包
配置事务管理器
1
2
3
4
5<!-- 配置平台事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入连接池 -->
<property name="dataSource" ref="dataSource"/>
</bean>开启注解事务
1
2<!-- 开启注解事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>在业务层添加注解
运行测试