Spring的事务的实现方式有两种:编程式事务跟声明式事务,其中声明式事务主要是通过配置文件或者注解的方式来进行编写的。但是在实际开发中都是用的声明式事务,所以本文主要讲解的是spring声明式事务如何配置以及一些踩坑点。在进行spring的事务管理之前,首先需要了解什么是事务呢?事务的特性以及并发事务的隔离又有哪些呢?
事务
事务是用户定义的一系列数据库操作,这些操作要么全都执行,要么全都不执行,是一个不可分割的工作单位。在关系数据库中,事务可以是一条SQL语句、一组SQL语句。以上定义摘自《数据库系统概论》一书。简单来说,就是用户只要对数据库进行了操作,这些操作都可看做是一个事务。
事务的特性
事务具有4个特性:原子性、一致性、隔离性以及持久性。(ACID)
原子性(Atomicity):指的是事务不可分,事务是数据库操作的逻辑工作单位。
一致性(Consistency):指的是数据库操作前后的数据库中的数据要保持一致性完整性,即从一个一致性状态转化为另一个一致性状态。
隔离性(Isolation):在并发执行下各事务之间互不干扰,一个事务的执行不能被其他事务所干扰
持久性(Durability):一个事务提交之后对数据的改变将是永久性的。
并发事务产生的问题
并发事务下可能会产生入下的问题:
1、脏读:一个事务读到了另一个事务未提交的数据。
比如有两个事务T1和T2,事务T2数据库的内容进行了修改但还未提交,此时T1进来读取,读取到的就是事务T2修改未提交的数据,但此时T2将事务回滚的话,此时的更新数据将会全部被撤销,而事务T1并不知道T2进行了回滚操作,所以此时的T1读取到的就是脏数据。
2、不可重复读:一个事务读到了另一个事务已提交的更新(update)的数据。导致一个事务中多次的查询结果不一致。
比如有两个事务T1和T2,事务T1进行了第一次的查询,之后此时T2进来并对数据进行了更新操作,此时T1再次进行查询,就会发现两次的查询内容不一致,第一次查询到的内容不能重现出来。
3、幻读:一个事务堵到了另一个事务已插入(insert)的数据。导致一个事务中的多次查询结果不一致。
比如有两个事务T1和T2,事务T1进行了第一次的查询,之后此时T2进来并对数据进行了插入操作,此时T1再次进行查询,就会发现无缘无故的多了几条数据,导致第一次的查询结果不能重现。
为了解决并发事务产生的问题,就需要并发的事务进行严格的控制。这就是事务的隔离级别。
事务的隔离级别
1、读未提交:不能解决任何并发问题。
2、读已提交:能避免脏读,但是不可重复读和幻读仍然存在。
3、可重复读:能避免脏读和不可重复读,但是幻读仍然存在。
4、串行化:可以避免所有的并发性问题。该级别其实就是不允许事务并发执行,不能并发执行的事务自然就没有事务并发产生的问题了。
经过上述对事务相关概念与知识点的介绍之后,下面就来看看Spring中的事务吧。
Spring中的事务
该篇博文一开始就说明了Spring事务有两种实现方式:编程式跟声明式。
下面就先来看看Spring的声明式事务吧
声明式事务
声明式事务通过主要是在配置文件中配置我们需要添加事务的方法,Spring底层就会自动的给我们指定的目标类下的目标方法加上事务管理,这里也体现出了Spring的AOP编程思想。
这里用日常生活中的转账作为案例进行演示:
需求:一开始用户1跟用户2都有1000块,之后用户1给用户进行转账,用该例子对添加事务与不添加事务的结果进行对比。
基于配置文件的事务开发
首先先设计一个简单的数据库表:
编写DAO接口
package TestTransaction;
public interface AccountDao {
//加钱
public void increaseMoney(int id,double money);
//减钱
public void decreaseMoney(int id,double money);
}
编写DAO的接口实现类
package TestTransaction;
import org.springframework.jdbc.core.JdbcTemplate;
//数据库操作模板,可利用里面提供的方法进行数据库操作
private JdbcTemplate jt;
public JdbcTemplate getJt() {
return jt;
}
public void setJt(JdbcTemplate jt) {
this.jt = jt;
}
//加钱
@Override
public void increaseMoney(int id, double money) {
// TODO Auto-generated method stub
String sql="update wage set money=money+? where id=?";
jt.update(sql, money,id);
}
//减钱
@Override
public void decreaseMoney(int id, double money) {
// TODO Auto-generated method stub
String sql="update wage set money=money-? where id=?";
jt.update(sql, money,id);
}
}
编写service接口
package TestTransaction;
//转账方法
public interface AccountService {
public void tranfer(int from,int to,double money);
}
编写service接口实现类
package TestTransaction;
public class AccountServiceImpl implements AccountService{
private AccountDao accountDao;
public void tranfer(int from, int to, double money) {
//转账的一方减钱
accountDao.decreaseMoney(from, money);
int i=1/0;//模拟数据操作中途出错
//转账对象加钱
accountDao.increaseMoney(to, money);
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
}
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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.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">
<!-- 读取外部配置文件的数据库配置信息 -->
<context:property-placeholder location="springTransaction/db.properties"/>
<!-- 将userdao放入spring容器 -->
<bean id="accountDao" class="TestTransaction.AccountDaoImpl">
<property name="jt" ref="jdbcTemplate"></property>
</bean>
<!--将dao注入service-->
<bean name="accountService" class="TestTransaction.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<!-- 数据源配置方法 通过读取外部文件配置 -->
<bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 将JdbcTemplate 放入spring容器 -->
<bean name="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
<!-- 事务管理器,封装了所有事务操作,依赖于连接池 -->
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务通知 -->
<tx:advice id="txAdivce" transaction-manager="transactionManager">
<tx:attributes>
<!--
哪些方法需要用到哪些事务
isolation:事务的隔离级别,默认为default(即由当前连接的数据库默认的隔离级别决定)
propagation:事务的传播行为,解决不同业务层方法之间的互调问题
-->
<tx:method name="tranfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>
<!--配织入,将spring写好的事务通知织入到切入点 -->
<aop:config>
<aop:pointcut expression="execution(* TestTransaction.AccountServiceImpl.tranfer(..))" id="txpc"/>
<aop:advisor advice-ref="txAdivce" pointcut-ref="txpc"/>
</aop:config>
测试代码
@Test
public void test() {
ApplicationContext ac=new ClassPathXmlApplicationContext("TestTransaction/ApplicationContextAop.xml");
AccountService accountService = (AccountService) ac.getBean("accountService");
accountService.tranfer(1, 2, 100d);
}
结果预测:在service的实现类那里故意设置了一个1/0的操作,就是为了演示事务方法在执行期间突然出错,这时候的运行结果。由于在我们在配置文件中已经配置了数据的操作,所以当运行到int a=1/0这句代码时,系统就会报除数为0的错误信息,虽然在这之前的accountDao.decreaseMoney(from, money);这句代码已经执行了,但是该方法体已经被添加了事务,一旦出现错,就会进行回滚操作,即将减钱的操作撤回,所以数据操作后的结果不变。如果我们配置的事务不成功的话,就会出现用户1减100块变成900,而因为执行了int a= 1/0,导致加钱的操作无法执行,所以用户2的钱仍会是1000,这就违反了数据库事务的一致性。
结果测试如下:
很明显可以看出程序报了1/0的错误,下面看看吗数据库中的数据
数据中的数据仍然不改变,所以我们的事务是配置成功了的。
基于注解式的事务开发
使用注解开发需要在配置文件中开启注解的功能,下面将上述的配置文件进行改写。
使用注解式配置事务的话在配置文件中就可以不用配置事务通知跟织入这两个配置,但要开启spring事务的注解配置。
改后的配置文件如下:
<!--在上述的配置文件中去掉事务通知跟织入的两个配置,然后开启注解,其他不变-->
<!-- 事务管理器,封装了所有事务操作,依赖于连接池 -->
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启事务注解配置 -->
<tx:annotation-driven/>
由于在配置文件中去掉了事务通知和织入的配置,所以需要在业务层中使用注解的方式来标注。
service层代码修改如下
以上配置是还没有添加事务配置的情况下的配置文件,现在演示没有添加事务的执行情况
最后简单讲一下编程式事务
//使用@Transactional注解该方法,表示该方法为事务方法
@Transactional(isolation=Isolation.REPEATABLE_READ,propagation=Propagation.REQUIRED,readOnly=false)
public class AccountServiceImpl implements AccountService{
private AccountDao accountDao;
public void tranfer(int from, int to, double money) {
//转账的一方减钱
accountDao.decreaseMoney(from, money);
int i=1/0;
//转账对象加钱
accountDao.increaseMoney(to, money);
}
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
}
测试代码跟测试思路跟上述一样,这里就不加演示了。
编程式事务
对于编程式事务这里不做过多的讲述,只是简单的简单的演示核心代码。编程式事务主要是使用的是Spring提供的TransactionTemplate事务模板。
public class AccountServiceImpl implements AccountService {
private AccountDao ad ;
//事务管理模板
private TransactionTemplate tt;
public void transfer2(final Integer from,final Integer to,final Double money) {
//将需要添加事务的代码包装起来
tt.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus arg0) {
//减钱
ad.decreaseMoney(from, money);
int i = 1/0;
//加钱
ad.increaseMoney(to, money);
}
});
//将事务管理模板注入进来
public void setTt(TransactionTemplate tt) {
this.tt = tt;
}
}
<!--使用编程式的话需要在xml文件中配置事务模板-->
<!-- 事务模板对象 -->
<bean name="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate" >
<property name="transactionManager" ref="transactionManager" ></property>
</bean>
使用编程式事务进行事务管理的时候都将我们的添加事务的代码包在execute方法里面,但如果有很多需要事务管理的业务方法,这种编程方式就不可取了,不仅存在大量的模板代码,而且不利于开发和维护。
总结
1、Spring的事务管理是AOP编程思想的一种很好的体现。
2、在写该篇博客的过程中也遇到了一些坑,最大的坑就是事务不提作用这个问题。这里指出几种事务失效的原因。
a、使用mysql创建数据库时,其默认的数据库引擎是MyISAM的,必须将数据库引擎修改成InnoDB,只有InnoDB才能支持事务操作(这也是本人遇到的情况)
b、要使用事务的方法必须是public修饰的
c、同个service层下,非事务方A法调用了事务方法B,但是在测试的时候调用的是非事务方法A,这时事务也会失效。
如果还有其他原因引起事务失效的话,可以在评论区评论哦~
好了,spring的事务讲解就到此结束了,如有写错的地方欢迎指出,本人必当虚心请教,谢谢啦~