Spring的事务管理
事务概念:
什么是事务:
事务就是多个数据库基本操作的集合体。集合后就是一个事务单元,事务操作就是,要么都成功,要么都失败。
要是对事务没有了解可以查看:
事务管理API
Spring事务管理两种方式
第一种:编程式事务管理(不用)
第二种:声明式事务管理。
声明式事务管理分为两种:
1.基于xml配置文件实现
2.基于注解实现
Spring事务管理API介绍
Spring事务管理高层抽象主要包括3个接口
PlatformTransactionManager 事务管理器
TransactionDefinition 事务定义信息(隔离、传播、超时、只读)
TransactionStatus 事务具体运行状态
我们这里使用PlatformTransactionManager(事务管理器)
Spring为不同的持久化框架提供了不同PlatformTransactionManager(事务管理器)接口实现
事务 | 说明 |
---|---|
org.springframework.jdbc.datasource.DataSourceTransactionManager | 使用Spring JDBC或 IBatis 进行持久化时间时使用 |
org.springframework.orm.hibernate5.HibernateTransactionManager | 使用Hibernate5.0版本进行持久化数据时使用 |
org.springframework.orm.jpa.JpaTransactionManager | 使用JPA进行持久化时使用 |
org.springframework.jdo.JdoTransactionManager | 当持久化机制是Jdo时使用 |
org.springframework.transaction.ita.ItaTransactionManager | 使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用 |
不管是使用xml配置文件实现还是基于xml注解实现,都要首先配置事务管理器
下面来个需求:账户转账
首先搭建转账环境:用c3p0和JdbcTemplate实现基本的转账功能
创建数据库表,添加数据:
create table zhanghu(
id int primary key,
username char(20),
salary int);
insert into zhanghu values(1,'FireLang',10000);
insert into zhanghu values(2,'LangShen',12300);
创建Service和Dao类完成注入:
1.Service层也被叫做业务逻辑层。
2.dao层,单纯对数据库操作层,在dao层不添加业务。这些都是一种规范。
这是dao层:
@Component(value="moneyDao")
public class MoneyDao {
@Resource(name="jdbcTemplate")
private JdbcTemplate jdbcTemplate;
public void reduceMoney(String name,int reduceNum){
String sql="update zhanghu set salary=salary-? where username=?";
jdbcTemplate.update(sql, reduceNum,name);
}
public void addMoney(String name,int addNum){
String sql="update zhanghu set salary=salary+? where username=?";
jdbcTemplate.update(sql, addNum,name);
}
}
这是Service层:
@Service(value="moneyService")
public class MoneyService {
@Resource(name="moneyDao")
private MoneyDao moneyDao;
public void transferAccounts(String reduceName,String addName,int num){
moneyDao.reduceMoney(reduceName, num);
moneyDao.addMoney(addName, num);
}
}
这是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: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.xsd">
<!-- 开启JavaBean注释扫描 -->
<context:component-scan base-package="cn.domarvel"></context:component-scan>
<!-- 创建c3p0连接池对象,并且初始化c3p0连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/template"></property>
<property name="user" value="root"></property>
<property name="password" value=""></property>
</bean>
<!-- 创建jdbcTemplate,并且通过IOC的依赖注入的构造注入方式注入c3p0连接池对象,那么获取到jdbcTemplate后就能够直接使用 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 构造注入 -->
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
</beans>
测试代码:
public class MoneyServiceTest {
@Test
public void transferAccounts(){
ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
((MoneyService)context.getBean("moneyService")).transferAccounts("LangShen", "FireLang", 1000);
}
}
问题出现:
在这里我们已经成功的完成了转账,但是如果在Service层的转账方法里面,在转账过程中出现了转账的异常,比如我现在给发起方扣除了钱,但是现在突然现在机房断电了,后续的加钱操作也就没有了。出现了转账错误,钱凭空消失的问题,给用户会造成很大的困扰。
解决方法:
所以到了这里就需要事务的操作了。在事务操作中,如果我们在扣除钱后,加钱之前出现了突然断电情况,那么当数据库再次开启后,这个事务是会回滚的,因为事务的特性就是,在事务里面的操作,要么全部成功,要么全部都不成功,不成功的是会事务回滚,回滚到操作之前。其中回滚是按照数据库日志回滚的。
下面我们就用到Spring的事务操作:
我们用到了Spring的声明式事务管理:
两种方式都会讲到:
1.基于xml配置文件实现
2.基于注解实现
要用Spring的事务,就得导包,以及在Spring核心配置文件中修改约束:
对于导包,你就导入JdbcTemplate包和spring-tx包就行了。 下载地址
这里用到的连接池是c3p0 下载地址
因为在Spring的事务操作中会用到aop思想,所以我们还要导入AspectJ包,以及Spring对AspectJ的依赖包。
约束我已经整理好了:
<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">
<!-- 最后一个tx的就是Spring事务的约束 -->
声明式事务管理 (XML配置)
配置文件方式使用aop思想配置
第一步: 配置事务管理器(在核心Spring配置文件中进行配置)
<!-- 配置事务管理器 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入dataSource -->
<property name="dataSource" ref="dataSource"></property>
</bean>
第二步: 配置事务增强
<!-- 第二步 配置事务增强 -->
<tx:advice id="txadvice" transaction-manager="dataSourceTransactionManager">
<!-- 做事务操作 -->
<tx:attributes>
<!-- 设置进行事务操作的方法匹配规则 -->
<tx:method name="transfer*" propagation="REQUIRED"/>
<!-- 这里的propagation属性值如果是REQUIRED就可以不用写,因为默认就是REQUIRED -->
<!-- name属性值可以用*号来匹配方法,匹配的方法是切入点里面的方法。当然如果你只想匹配某一个切入点里面的方法,你可以就直接写方法名 -->
</tx:attributes>
</tx:advice>
第三步: 配置切面
<!-- 第三步 配置切面 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut expression="execution(* cn.domarvel.service.MoneyService.transferAccounts(..))" id="pointcut1"/>
<!-- 这个事务增强的过程就是在配置的切入点里面找到要进行增强事务的方法。 -->
<!-- 切面 -->
<aop:advisor advice-ref="txadvice" pointcut-ref="pointcut1"/>
</aop:config>
整体代码:
<?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: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">
<!-- 开启JavaBean注释扫描 -->
<context:component-scan base-package="cn.domarvel"></context:component-scan>
<!-- 创建c3p0连接池对象,并且初始化c3p0连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/template"></property>
<property name="user" value="root"></property>
<property name="password" value=""></property>
</bean>
<!-- 创建jdbcTemplate,并且通过IOC的依赖注入的构造注入方式注入c3p0连接池对象,那么获取到jdbcTemplate后就能够直接使用 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 构造注入 -->
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
<!-- 第一步 配置事务管理器 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入dataSource -->
<!-- 这里不注入dataSource,别人怎么帮你增强啊 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 第二步 配置事务增强 -->
<tx:advice id="txadvice" transaction-manager="dataSourceTransactionManager">
<!-- 做事务操作 -->
<tx:attributes>
<!-- 设置进行事务操作的方法匹配规则 -->
<tx:method name="transfer*" propagation="REQUIRED"/>
<!-- 这里的propagation属性值如果是REQUIRED就可以不用写,因为默认就是REQUIRED -->
<!-- name属性值可以用*号来匹配方法,匹配的方法是切入点里面的方法。当然如果你只想匹配某一个切入点里面的方法,你可以就直接写方法名 -->
<!-- 大家应该知道我们在配置切面的时候已经配置了要对哪儿些方法进行事务操作,execution(* cn.domarvel.service.MoneyService.*(..)) -->
<!-- 为什么在method name="transfer*"这里还要进行切入点的方法选择呢?? -->
<!-- 因为这里可以对方法进行优化,比如: -->
<!--
<tx:method name="*" propagation="REQUIRED" rollback-for="java.lang.Exception" isolation="DEFAULT" />
<tx:method name="get*" read-only="true" />
这两种配置对于执行速度是不一样的。
第二种很明显速度是很快的。read-only对表是没有加锁的。
-->
</tx:attributes>
</tx:advice>
<!-- 第三步 配置切面 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut expression="execution(* cn.domarvel.service.MoneyService.*(..))" id="pointcut1"/>
<!-- 切面 -->
<aop:advisor advice-ref="txadvice" pointcut-ref="pointcut1"/>
</aop:config>
</beans>
tips:
在这里面还有几个常用属性:
<tx:method name="transfer*" propagation="REQUIRED"/>
比如:rollback-for,propagation(事务传播级别)
read-only(true-false):
- 只读事务”并不是一个强制选项,它只是一个“暗示”,提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。
- 但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。
- 因此,“只读事务”仅仅是一个性能优化的推荐配置而已,并非强制你要这样做不可。
- 对于数据库读来说,我们也可以在数据库中建立视图来进行读操作,这样也能够加速读的速度!!!
还有一个常用的属性isolation(事务隔离级别)
Spring事务的隔离级别
- ISOLATION_DEFAULT: 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.
另外四个与JDBC的隔离级别相对应 - ISOLATION_READ_UNCOMMITTED: 这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据。
这种隔离级别会产生脏读,不可重复读和幻像读。 - ISOLATION_READ_COMMITTED: 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据
- ISOLATION_REPEATABLE_READ: 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。
它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。 - ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。
除了防止脏读,不可重复读外,还避免了幻像读。
在这里要注意导入Spring的aop的依赖包AspectJ包。
注意Spring核心配置文件没有先后配置顺序。
好了现在事务设置好了,我们要准备出现异常了:
- 在MoneyService中修改代码
@Service(value="moneyService")
public class MoneyService {
@Resource(name="moneyDao")
private MoneyDao moneyDao;
public void transferAccounts(String reduceName,String addName,int num){
moneyDao.reduceMoney(reduceName, num);
int a=10/0;//在把钱减少后,给别人添加钱之前出现了异常,那么事务将会回滚,而不会单方面的少钱或者多钱。
moneyDao.addMoney(addName, num);
}
}
运行测试代码操作后,将不会单方面少钱或者多钱。
声明式事务管理(注解)
第一步:配置事务管理器
<!-- 第一步 配置事务管理器 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入dataSource -->
<property name="dataSource" ref="dataSource"></property>
</bean>
第二步:配置事务注解
<!-- 第二步 开启事务注解,它会去扫描所有类,看方法上面是否有对应的注解 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
整体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: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">
<!-- 开启JavaBean注释扫描 -->
<context:component-scan base-package="cn.domarvel"></context:component-scan>
<!-- 创建c3p0连接池对象,并且初始化c3p0连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/template"></property>
<property name="user" value="root"></property>
<property name="password" value=""></property>
</bean>
<!-- 创建jdbcTemplate,并且通过IOC的依赖注入的构造注入方式注入c3p0连接池对象,那么获取到jdbcTemplate后就能够直接使用 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 构造注入 -->
<constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>
<!-- 第一步 配置事务管理器 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入dataSource -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 第二步 开启事务注解 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>
第三步:在要使用事务的方法所在类上面添加注解
- 当标于类前时, 标示类中所有方法都进行事务处理,但是被处理的方法只能是public类型的。
@Service(value="moneyService")
@Transactional//在当前操作类上配置事务管理。默认就是处理该类中的所有方法。
public class MoneyService {
@Resource(name="moneyDao")
private MoneyDao moneyDao;
public void transferAccounts(String reduceName,String addName,int num){
moneyDao.reduceMoney(reduceName, num);
int a=10/0;//在把钱减少后,给别人添加钱之前出现了异常,那么事务将会回滚,而不会单方面的少钱或者多钱。
moneyDao.addMoney(addName, num);
}
}
如果想要某一个方法不加入事务处理,只需要像下面这样:
@Service(value="moneyService")
@Transactional
public class MoneyService {
@Resource(name="moneyDao")
private MoneyDao moneyDao;
@Transactional(propagation = Propagation.NOT_SUPPORTED)//让当前方法不加入事务处理
public void transferAccounts(String reduceName,String addName,int num){
moneyDao.reduceMoney(reduceName, num);
int a=10/0;//那么没有事务处理的话,就会单方面的少钱或者多钱,比如这里就会单方面少钱。
moneyDao.addMoney(addName, num);
}
}
上面类中事务的处理行为有个专业名词叫做事务的传播行为。
事务传播行为介绍:
@Transactional(propagation=Propagation.REQUIRED)
如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED)
容器不为这个方法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW)
不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY)
必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.NEVER)
必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.SUPPORTS)
如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
事务超时设置:
@Transactional(timeout=30) //默认是30秒
事务传播可以参考:
MySQL隔离级别和事务传播(隔离级别这个作者有些地方讲错了,但是事务传播讲的还不错,主要是图不错)