五、Spring中的事务管理
1. 事务简介
- 事务管理是企业级应用程序开发中必不可少的技术,事务是用来确保数据的完整性和一致性
- 事务就是一系列的动作,当做一个独立的工作单元,这些动作要么全部成功,要么全部失败
- 事务的四个关键属性(ACID)
- 原子性(atomicity):事务是一个不可分割的工作单元,事务中包括的诸操作要么都做,要么都不做
- 一致性(consistency):一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务规则的一致性状态中
- 隔离性(isolation):可能有许多事务会同时处理相同的数据,因此每个事物都应该与其他事务隔离开来,防止数据损坏
- 持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响
2. 事务管理的问题
Connection conn = DBUtil.getConnection();
PreparedStatement stmt = null;
String sql = "insert into ...";
try {
//修改事务的提交方式为手动提交
conn.setAutoCommit(false);
stmt = conn.prepareStatement(sql);
//设置占位符
stmt.setString(1, ...);
stmt.executeUpdate();
//最后手动提交
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
try{
//如果出现异常回滚
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
DBUtil.closeAll(conn, stmt, null);
}
必须为不同的方法重写类似的样板代码。并且这段代码是特定于JDBC的,一旦选择类其它数据库存取技术,代码需要作出相应的修改
3. Spring中的事务管理
- 作为企业级应用程序框架,Spring在不同的事务管理API之上定义了一个抽象层。而应用程序开发人员不必了解底层的事务管理API,就可以使用Spring的事务管理机制
- Spring既支持编程式事务管理,也支持声明式的事务管理
- 编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在编程式管理事务时,必须在每个事务操作中包含额外的事务管理代码
- 声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。Spring通过AOP框架支持声明式事务管理
4. 事务管理器
- Spring从不同的事务管理API中抽象了一整套的事务机制。开发人员不必了解底层的事务API,就可以利用这些事务机制。有了这些事务机制,事务管理代码就能独立于特定的事务技术了
- Spring的核心事务管理器是PlatformTransactionManager接口。它为事务管理封装了一组独立于技术的方法,无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的
JDBC的事务管理器:DataSourceTransactionManager
Hibernate的事务管理器:HibernateTransactionManager
5. 事务案例
5.1 命名空间和标签规范
在spring的配置文件中加入tx命名空间和标签规范
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
5.2 Spring的配置
<?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:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
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/util
http://www.springframework.org/schema/util/spring-util.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:component-scan base-package="com.spring"/>
<!-- 读取资源文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 连接数据库的基础信息 -->
<property name="driverClassName" value="${jdbc.oracle.driver}"/>
<property name="url" value="${jdbc.oracle.url}"/>
<property name="username" value="${jdbc.oracle.username}"/>
<property name="password" value="${jdbc.oracle.password}"/>
</bean>
<!-- 配置JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
5.3 建立表结构
create table T_ACCOUNT
(
id NUMBER not null,
name VARCHAR2(20),
balance NUMBER(8,2),
primary key (ID)
);
insert into T_ACCOUNT (id, name, balance) values (1, '小明', 1000);
insert into T_ACCOUNT (id, name, balance) values (2, '淘宝', 0);
insert into T_ACCOUNT (id, name, balance) values (3, '京东', 0);
commit;
5.4 实体类
public class Account {
private Integer id;
private String name;
private Double balance;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
5.5 Dao
接口
public interface AccountDao {
void updateBalance(Account account);
Account findAccountById(Integer id);
}
实现类
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void updateBalance(Account account) {
String sql = "update t_account set balance=? where id=?";
jdbcTemplate.update(sql, account.getBalance(), account.getId());
System.out.println("对账户的余额更新操作完成..........");
}
@Override
public Account findAccountById(Integer id) {
String sql = "select id,name,balance from t_account where id=?";
RowMapper<Account> rowMapper = new BeanPropertyRowMapper<>(Account.class);
return jdbcTemplate.queryForObject(sql, rowMapper, id);
}
}
5.6 Service
接口
public interface AccountService {
void transferMoney(int fromId, int toId, double money) throws Exception;
}
实现类
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void transferMoney(int fromId, int toId, double money) throws Exception {
/**
* 在转账业务逻辑中包含以下的几个步骤
* 1.查询转出账户的余额
* 2.从转出账户中扣除转账金额
* 3.查询转入账户的余额
* 4.转入账户的余额+转账金额,并更新转入账户的余额
*/
Account fromAccount = accountDao.findAccountById(fromId);
fromAccount.setBalance(fromAccount.getBalance()-money);
accountDao.updateBalance(fromAccount);
//模拟程序执行过程中出现问题,异常出现导致程序终止(方法结束)
//System.out.println(10/0);
Account toAccount = accountDao.findAccountById(toId);
toAccount.setBalance(toAccount.getBalance()+money);
accountDao.updateBalance(toAccount);
System.out.println("一次转账完成...");
}
}
5.7 无事务测试
public class TXTest {
@Test
public void testTransfer() throws ClassNotFoundException {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = ac.getBean("accountService", AccountService.class);
accountService.transferMoney(1,2,360);
}
}
测试结果分析:
- 当Service的方法中没有异常,程序正常执行完毕,数据库中的数据也是正常的
小明账户中的余额为640,淘宝账户中的余额为360 - 当Service的方法中出现异常,程序因异常终止执行,数据库中的数据出现问题(数据不一致)
小明账户中的余额为640,淘宝账户中的余额为0,转出的金额数据丢失
5.8 配置事务管理
配置事务管理器
<!-- 配置事务管理器-->
<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="*"/>
</tx:attributes>
</tx:advice>
配置事务切入点
<!-- 配置事务切入点:通过AOP实现事务管理-->
<aop:config>
<aop:pointcut id="exp" expression="execution(* com.spring.service.*.*(..))"/>
<!-- 将切点表达式应用在事务上-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="exp"/>
</aop:config>
事务测试
当Service的方法中出现异常,程序因异常终止执行,数据库中的数据仍然为原有数据
小明账户中的余额为1000,淘宝账户中的余额为0,并没有实施金额的转出与转入(执行成功的部分数据回滚)
6. 事务的属性
6.1 事务的传播行为
- 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行
- 事务的传播行为可以由传播属性指定,在
<tx:method>
标签中配置propagation属性 - Spring定义了7种类传播行为:其中REQUIRED为默认值
传播属性 | 描述 |
---|---|
REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行 |
REQUIRES_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行。如果有事务正在运行,应该将它挂起 |
SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行。否则它可以不运行在事务中 |
NOT_SUPPORTED | 当前的方法不应该运行在事务中。如果有运行的事务,将它挂起 |
MANDATORY | 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常 |
NEVER | 当前的方法不应该运行在事务中。如果有运行的事务,就抛出异常 |
NESTED | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行。否则,就启动一个新的事务,并在它自己的事务内运行 |
REQUIRED传播行为
当事务method2()方法与method3()方法被另一个事务方法method1()调用时,它默认会在现有的事务内运行,这个默认的传播行为就是REQUIRED。因此在method1()方法的开始和终止边界内只有一个事务,这个事务只在method1()方法结束的时候被提交,如果执行过程中出现异常,导致3个方法的数据都要回滚。
REQUIRES_NEW传播行为
传播行为REQUIRES_NEW是当事务method2()方法与method3()方法被另一个事务方法method1()调用时,method2()与method3()都必须启动一个新事务,并且在method2()与method3()的事务执行时,method1()方法的事务会先挂起,等待其他事务执行完成,再继续执行。执行过程中出现异常,如果method2()或method3()的事务已经执行完成,那么它们是不会回滚数据,只有method1()的数据要回滚。
注意:Spring的事务管理通过切面实现,如果直接使用this.方法()或者方法(),不会触发切面中对事务的管理。需要将方法定义不同的类中,或者service自己注入自己,用这个注入对象来调用方法。
传播行为测试
在Service层添加payOrder的方法,实现批量转账功能。
接口
public interface AccountService {
void payOrder(int fromId, Map<Integer, Double> map) throws Exception;
}
实现类
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
/**
* 注意:
* 同一个Service中不同方法在进行相互调用,不能另起事务,必须在同一个事务内运行
* 如果需要单独启动事务,需要当前Service类定义一个自己类型属性,并将自己的对象注入
* 通过该属性调用方法,即可
*/
@Autowired
private AccountService accountService;
@Override
public void payOrder(int fromId, Map<Integer, Double> map) throws Exception {
Set<Integer> toIdSet = map.keySet();
for (Integer toId : toIdSet) {
double money = map.get(toId);
//实施单账户转账
accountService.transferMoney(fromId, toId, money);
//模拟第一次转账之后,系统出现异常
String s = null;
s.trim();
}
System.out.println("订单支付成功...");
}
}
事务属性配置
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 事务属性-->
<tx:attributes>
<tx:method name="transferMoney" propagation="REQUIRES_NEW"/>
<!-- 所有的事务管理的方法中,采用默认的事务属性 -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
测试
@Test
public void testPayOrder() throws Exception {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = ac.getBean("accountService", AccountService.class);
Map<Integer, Double> map = new HashMap<>();
map.put(2, 199.0);
map.put(3, 260.0);
accountService.payOrder(1, map);
}
测试结果分析:
- 当payOrder方法中出现异常,传播行为是REQUIRED时,数据库中的数据仍然为原有数据,并没有发生变化
第一次成功转账的数据也随着异常一起回滚 - 当payOrder方法中出现异常,传播行为是REQUIRES_NEW时,数据库中的数据发生变化
第一次成功转账的数据提交并没有回滚
6.2 事务的隔离级别
- 当同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时,可能导致的问题可以分为下面三种类型:
- 脏读:对于两个事务TX1和TX2,TX1读取了已经被 TX2更新但还没有被提交的数据。之后,如果TX2回滚,TX1读取的内容就是临时且无效的
- 不可重复读:对于两个事务TX1和TX2,TX1读取了一个字段,然后TX2更新了该字段。之后,TX1再次读取同一个字段,值就不同了
- 幻读:对于两个事务TX1和TX2,TX1从一个表中读取了读取几行数据,然后TX2在该表中插入了一些新的记录。之后,如果TX1再次读取同一个表,就会多出几行
- 从理论上来说,事务应该彼此完全隔离,以避免并发事务所导致的问题。然而,那样会对性能产生极大的影响,因为事务必须按顺序运行。所以在实际开发中,为了提升性能,事务会以较低的隔离级别运行
- 事务的隔离级别可以通过隔离事务属性指定,在
<tx:method>
标签中配置isolation属性,其默认值为:DEFAULT
隔离级别 | 描述 |
---|---|
DEFAULT | 使用底层数据库的默认隔离级别。对于大多数数据库来说,默认隔离级别都是READ_COMMITTED |
READ_UNCOMMITTED | 允许事务读取未被其他事物提交的变更。脏读,不可重复读和幻读的问题都会出现 |
READ_COMMITTED | 只允许事务读取已经被其它事务提交的变更。可以避免脏读,但不可重复读和幻读问题仍然可能出现 |
REPEATABLE_READ | 确保事务可以多次从一个字段中读取相同的值。在这个事务持续期问,禁止其他事物对这个字段进行更新。可以避免脏读和不可重复读,但幻读的问题仍然存在 |
SERIALIZABLE | 确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入,更新和删除操作。所有并发问题都可以避免,但性能十分低下 |
- 事务的隔离级别要得到底层数据库引擎的支持,而不是应用程序或者框架的支持
- Oracle支持的2种事务隔离级别:READ_COMMITTED,SERIALIZABLE,默认为READ_COMMITTED
- Mysql支持4中事务隔离级别,默认为REPEATABLE_READ
mysql事务隔离级别相关语句:
-- 事务自动提交
show variables like 'autocommit';
-- 关闭OFF
set autocommit = 0;
-- 开启ON
set autocommit = 1;
-- 查看系统级的隔离级别和会话级的隔离级别
select @@global.tx_isolation,@@tx_isolation;
-- 设置事务的隔离级别:set 作用域 transaction isolation level 事务隔离级别名称
-- set [session | global] transaction isolation level {read uncommitted | read committed | repeatable read | serializable}
-- 全局
set global transaction isolation level read committed;
-- 当前会话
set session transaction isolation level read uncommitted;
oracle事务隔离级别相关语句:
-- 设置一个事务的隔离级别:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 设置单个会话的隔离级别:
ALTER SESSION SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
ALTER SESSION SET TRANSACTION ISOLATION SERIALIZABLE;
-- 首先创建一个事务
declare
trans_id Varchar2(100);
begin
trans_id := dbms_transaction.local_transaction_id( TRUE );
end;
-- 查看事务隔离级别
SELECT s.sid, s.serial#,
CASE BITAND(t.flag, POWER(2, 28))
WHEN 0 THEN 'READ COMMITTED'
ELSE 'SERIALIZABLE'
END AS isolation_level
FROM v$transaction t
JOIN v$session s ON t.addr = s.taddr AND s.sid = sys_context('USERENV', 'SID');
6.3 回滚事务属性
- 默认情况下只有运行时异常(RuntimeException)会导致事务回滚,而编译期异常不会
- 事务的回滚规则可以通过,
<tx:method>
标签中的rollback-for和no-rollback-for属性来配置,如果有不止一种异常,用逗号分隔 - rollback-for:遇到时必须进行回滚
- no-rollback-for:遇到时必须不回滚
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 事务属性-->
<tx:attributes>
<tx:method name="transferMoney" propagation="REQUIRED" isolation="READ_COMMITTED"
rollback-for="java.lang.ClassNotFoundException"
no-rollback-for="java.lang.NullPointerException"/>
<!-- 所有的事务管理的方法中,采用默认的事务属性 -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
6.4 只读事务属性
<tx:method>
标签中的read-only="true"指定事务为只读事务,表示该事务只读取数据而不更新数据,可以帮助数据库优化事务。如果该方法中只是读取数据库中的数据,那么应该将其设置为只读事务
6.5 事务超时属性
- 事务在强制回滚之前可以保持多久,这样可以防止长期运行的事务占用资源
<tx:method>
标签中的timeout属性配置事务的超时,其值为int类型,单位为秒钟,如果值为-1表示永不过期- 两个事务先后访问同一个数据,此时先到事务将数据隔离,那么后到的事务将无法操作数据,后到事务就会进入等待,等待以及执行时间为此事务的过期时间。如果事务过期那么此事务会强制回滚
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 事务属性-->
<tx:attributes>
<!--
配置事务的方法名称如果以find开头,那么它的事务传播行为为:SUPPORTS
-->
<tx:method name="insert*" isolation="READ_COMMITTED"/>
<tx:method name="add*" isolation="READ_COMMITTED"/>
<tx:method name="save*" isolation="READ_COMMITTED"/>
<tx:method name="update*" isolation="READ_COMMITTED"/>
<tx:method name="edit*" isolation="READ_COMMITTED"/>
<tx:method name="delete*" isolation="READ_COMMITTED"/>
<tx:method name="remove*" isolation="READ_COMMITTED"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="list*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="load*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="transferMoney" propagation="REQUIRED" isolation="READ_COMMITTED"
rollback-for="java.lang.ClassNotFoundException"
no-rollback-for="java.lang.NullPointerException"
timeout="4"/>
<!-- 所有的事务管理的方法中,采用默认的事务属性 -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>