一.事务简介
在学些spring的事务管理之前,我们先来看看数据库的事务!
事务(transaction):是一系列对系统中数据进行访问与更新的操作组成的一个程序执行逻辑单元。
下面是事务管理的思维导图:详情可以查看文章,https://www.jianshu.com/p/aa35c8703d61
下面我们进入主题,开始学习spring的事务管理。
Spring采用AOP机制完成事务控制,可以实现在不修改原有组件的代码情况下实现事务控制功能。
二.Spring对事务管理的支持
sping提供了两种事务管理方式:编程式事务管理和声明式事务管理。
1.编程式事务
场景:
现在有用户表user(id,username,pwd、name、gender);
用户的记录表record(record_id,user_id,remark,status),其中status字段类型为int。
现在要删除某个一个用户,同时修改这个用户记录的status为废弃。
通过编码方式实现事务管理。
Spring实现编程式事务要依赖两大类:PlatformTransactionManager类和TransactionTemplate类。我们在之前搭好的springMVC项目的基础上来做,前面的文章基于Spring MVC的前后端分离开发和Spring中的AOP任意一个的基础上都行。
(1).PlatformTransactionManager类
a.配置数据源事务管理
在spring容器配置文件applicationContext.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:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 开启组件扫描 -->
<context:component-scan base-package="com.cdd" />
<!-- 配置要用到其他bean,包括handlerMapping,handlerAdapter等bean,支持@RequestMapping,@RequestBody等注解 -->
<mvc:annotation-driven/>
<!-- 配置数据库连接池 -->
<bean id="dbcp" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/jsd15077db?useUnicode=true&characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 注册jdbcTemplate的Bean -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 注册组件bean -->
<bean id="printLog" class="com.cdd.aspect.PrintLog"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切面组件 -->
<aop:aspect ref="printLog">
<!-- 在进入com.cdd.controller包下组件之前(切点),先调用切面组件的callTime方法 -->
<aop:before method="callTime" pointcut="within(com.cdd.controller.*)"/>
</aop:aspect>
</aop:config>
<!-- 开启aop注解支持,@Aspect,@通知标记 -->
<aop:aspectj-autoproxy />
<!-- 配置数据源事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dbcp"></property>
</bean>
</beans>
其他的配置在前面两篇文章介绍时已经写好了,我们这次要配置的如下:
b.在com.cdd.dao包下,新建一个dao组件,名称是 TransactionDao,代码如下:
package com.cdd.dao;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Repository
public class TransactionDao {
@Resource
private PlatformTransactionManager ptm;
@Resource
private DataSource dataSource;
@Resource
private JdbcTemplate jdbcTemplate;
private final static String USER_SQL = "DELETE FROM user WHERE id=4";
private final static String RECORD_SQL = "UPDATE record SET status='hello' WHERE user_id=4";
public void testTransaction1(){
// 定义事务
DefaultTransactionDefinition dtd = new DefaultTransactionDefinition();
//设置隔离机制
dtd.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
//设置传播行为
dtd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//获取事务状态
TransactionStatus status = ptm.getTransaction(dtd);
try{
//DML
int i = jdbcTemplate.update(USER_SQL);
int j = jdbcTemplate.update(RECORD_SQL);
System.out.println(i+","+j);
ptm.commit(status);
System.out.println("没问题,提交了");
}catch(Exception e){
ptm.rollback(status);
System.out.println("发生异常回滚了");
e.printStackTrace();
}
}
}
我们可以看到在上面代码中,RECORD_SQL语句中把status字段更新为“hello”字符串,这个字段类型为int,所以执行时,肯定会发生异常。
c.测试
我们在com.cdd.test包下编写测试类TestTransaction类,代码如下:
package com.cdd.test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.cdd.dao.TransactionDao;
public class TestTransaction {
public static void main(String[] args){
String conf = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(conf);
TransactionDao dao = ac.getBean("transactionDao",TransactionDao.class);
dao.testTransaction1();
}
}
d.查看事务控制是否成功
查看数据库user表和record表如下:
发现这两条记录都还在,说明事物控制成功了,接下来看控制台:
的确发生了异常,回滚了,也打印出来 “发生异常回滚了” 这句话。
(2).TransactionTemplate类
a.spring容器中的配置上面已经完成了,现在在TransactionDao类中添加testTransaction2方法,代码如下:
package com.cdd.dao;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Repository
public class TransactionDao {
@Resource
private PlatformTransactionManager ptm;
@Resource
private DataSource dataSource;
@Resource
private JdbcTemplate jdbcTemplate;
private final static String USER_SQL = "DELETE FROM user WHERE id=4";
private final static String RECORD_SQL = "UPDATE record SET status='hello' WHERE user_id=4";
//测试PlatformTransactionManager类实现事务控制
public void testTransaction1(){
// 定义事务
DefaultTransactionDefinition dtd = new DefaultTransactionDefinition();
//设置隔离机制
dtd.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
//设置传播行为
dtd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//获取事务状态
TransactionStatus status = ptm.getTransaction(dtd);
try{
//DML
int i = jdbcTemplate.update(USER_SQL);
int j = jdbcTemplate.update(RECORD_SQL);
System.out.println(i+","+j);
ptm.commit(status);
System.out.println("没问题,提交了");
}catch(Exception e){
ptm.rollback(status);
System.out.println("发生异常回滚了");
e.printStackTrace();
}
}
//测试TransactionTemplate类实现事务控制
public void testTransaction2(){
//实例化事务目标类
TransactionTemplate tt = new TransactionTemplate(ptm);
//设置隔离机制
tt.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
//设置传播行为
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//重写事务回调方法
TransactionCallbackWithoutResult tcw = new TransactionCallbackWithoutResult(){
@Override
protected void doInTransactionWithoutResult(TransactionStatus arg0) {
//DML
int i = jdbcTemplate.update(USER_SQL);
int j = jdbcTemplate.update(RECORD_SQL);
}
};
tt.execute(tcw);
}
}
就是这部分:
b.测试
在testTransaction类用调用testTransaction2()方法,代码如下:
package com.cdd.test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.cdd.dao.TransactionDao;
public class TestTransaction {
public static void main(String[] args){
String conf = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(conf);
TransactionDao dao = ac.getBean("transactionDao",TransactionDao.class);
// //测试PlatformTransactionManager类实现事务控制
// dao.testTransaction1();
//测试TransactionTemplate类实现事务控制
dao.testTransaction2();
}
}
c.查看是否成功
先看数据库,这两条记录还在
再看控制台:
发生异常后,的确回滚了,事务控制成功。在编程式事务管理中,这种方式推荐使用。
2.声明式事务
场景:
我们要用声明式事务的方式,给com.cdd.dao包下的UserDao组件的checkLogin()方法配置事务。
(1).xml方式配置
a.在spring容器进行aop配置
这里的配置是接着上面的声明式配置的,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:util="http://www.springframework.org/schema/util" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 开启组件扫描 -->
<context:component-scan base-package="com.cdd" />
<!-- 配置要用到其他bean,包括handlerMapping,handlerAdapter等bean,支持@RequestMapping,@RequestBody等注解 -->
<mvc:annotation-driven/>
<!-- 配置数据库连接池 -->
<bean id="dbcp" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/jsd15077db?useUnicode=true&characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 注册jdbcTemplate的Bean -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 注册组件bean -->
<bean id="printLog" class="com.cdd.aspect.PrintLog"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切面组件 -->
<aop:aspect ref="printLog">
<!-- 在进入com.cdd.controller包下组件之前(切点),先调用切面组件的callTime方法 -->
<aop:before method="callTime" pointcut="within(com.cdd.controller.*)"/>
</aop:aspect>
</aop:config>
<!-- 开启aop注解支持,@Aspect,@通知标记 -->
<aop:aspectj-autoproxy />
<!-- 配置数据源事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 配置切面aspect -->
<tx:advice id="txAdvice">
<tx:attributes>
<!-- 配置需要事务管理的方式(给单个方法配置) -->
<tx:method name="checkLogin"/>
<!-- 给以test开头的方法配置事务 -->
<!-- <tx:method name="check*"/> -->
<!-- 给所有方法配置事务 -->
<!-- <tx:method name="*"/> -->
</tx:attributes>
</tx:advice>
<!-- 配置aop -->
<aop:config>
<aop:pointcut expression="within(com.cdd.dao.UserDao)" id="target"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="target"/>
</aop:config>
</beans>
其他配置是前面已经配好的,在这个步骤我们要配置是下面这部分:
这个就是给com.cdd.dao包UserDao组件的checkLogin()方法配置事务
(2).注解方式配置
a.开启事务注解@Transacntional
我们先注释掉刚才xml形式配置的事务管理,再用声明式事务方式配置事务管理,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:util="http://www.springframework.org/schema/util" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 开启组件扫描 -->
<context:component-scan base-package="com.cdd" />
<!-- 配置要用到其他bean,包括handlerMapping,handlerAdapter等bean,支持@RequestMapping,@RequestBody等注解 -->
<mvc:annotation-driven/>
<!-- 配置数据库连接池 -->
<bean id="dbcp" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/jsd15077db?useUnicode=true&characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 注册jdbcTemplate的Bean -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 注册组件bean -->
<bean id="printLog" class="com.cdd.aspect.PrintLog"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切面组件 -->
<aop:aspect ref="printLog">
<!-- 在进入com.cdd.controller包下组件之前(切点),先调用切面组件的callTime方法 -->
<aop:before method="callTime" pointcut="within(com.cdd.controller.*)"/>
</aop:aspect>
</aop:config>
<!-- 开启aop注解支持,@Aspect,@通知标记 -->
<aop:aspectj-autoproxy />
<!-- 配置数据源事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 配置切面aspect -->
<tx:advice id="txAdvice">
<tx:attributes>
<!-- 配置需要事务管理的方式(给单个方法配置) -->
<tx:method name="checkLogin"/>
<!-- 给以test开头的方法配置事务 -->
<!-- <tx:method name="check*"/> -->
<!-- 给所有方法配置事务 -->
<!-- <tx:method name="*"/> -->
</tx:attributes>
</tx:advice>
<!-- 配置aop -->
<!-- <aop:config>
<aop:pointcut expression="within(com.cdd.dao.UserDao)" id="target"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="target"/>
</aop:config> -->
<!-- 开启事务注解@Transactionl,当调用@Tranactional标注的组件或方法时,将事务管理功能切入进去 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
在这个步骤中,注释的是:
在这个步骤中,我们添加的配置是:
b.使用@Transactional注解
给UserDao组件的checkLogin()方法打上@Transactional,即,对这个方法进行事务管理。如果给UserDao组件打上@Transactional注解,则给这个组件的所有方法进行事务管理,UserDao组件的代码如下:
package com.cdd.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository //标注数据库访问组件
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 验证用户登录
* @param name 用户登录名称
* @param pwd 密码
* @return
*/
@Transactional
public Integer checkLogin(String name, String pwd){
String sql = "select count(*) from user where username=? and pwd=?";
Object[] args = {name,pwd};
Integer isExist = jdbcTemplate.queryForObject(sql, Integer.class, args);
return isExist;
}
}
声明式事务是不是简单多了,我们推荐使用注解方法配置。就是现在说的这种方式。
三.spring对事务管理的控制
1.控制事务可读可写性
spring分为可读写事务和只读事务。
默认为可读写,一般只涉及查询操作建议用只读事务,用注解@Transactional(readOnly=ture) 只可读。
2.控制事务是否回滚
Spring遇到RuntimeExceptioin异常,会事务回滚;遇到非运行时异常不会回滚。
要在非异常时回滚,可用下面的注解方式(指定针对具体的非运行时异常回滚):@Transactional(rollbackFor=IOException.class) //遇到IOException时回滚
相反:
@Transactional(noRollbackFor=IOException.class) //遇到IOException时不回滚
建议:自定义异常继承运行时异常RuntimeException,这样默认情况下,自定义的异常发生时会回滚
例如:public MyException extends RuntimeException{ }
3.控制事务传播类型
spring的事务传播共有7种类型,如下表所示:
事务类型 | 含义 |
PROPAGATION_REQUIRED | 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘ |
PROPAGATION_MANDATORY | 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 创建新事务,无论当前存不存在事务,都创建新事务。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
4.控制事务隔离级别
(1).为什么要有事务隔离级别。
在一个项目中,如果事务T1和事务T2并发进行,同时操作一个数据。可能会发生哪些情况:
脏读(Dirty read):事务T1更新了数据,但是没提交。然后,事务T2读取了读取事务T1更新后的数据。接着,事务T1发生异常,数据回滚了。那么事务T2读到的数据就是无效的,就是脏数据。
不可重复读(Nonrepeatable read):事务T2读取了一次数据,之后,事务T1更新了这个数据,事务T2再次读取这个数据时,这时发现两次读取到的数据不一样。这就是不可重复读。
幻读(Phantom reads):事务T2在两次读取数据之间,事务T1向这个数据中insert了几条记录。事务T2发现第一次读的数据比第二次多几条,这就是幻读。和不可重复读是一种情况,只不过看的角度不一样。
那么为了避免这些问题的发生,我们不使用事务机制了?为了保证数据的一致性和完整性是事务我们一定要用的。还有什么办法避免这些问题,我们可以将这些事务彻底隔离,一个事务彻底执行完,提交或回滚后,再继续下一个事务。这样可以避免上面的问题,但是这样做的话又引发了新问题,效率低下,性能降低。那么,我们既要使用事务,又要保证性能,这时候,能控制事务重要性(不被别人更改可能性)的事务隔离级别就顺势而生了。
下表是事务的隔离级别:
隔离级别 | 含义 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别。 |
ISOLATION_READ_UNCOMMITTED | 允许读取尚未提交的更改。可能导致脏读、幻影读或不可重复读。 |
ISOLATION_READ_COMMITTED | 允许从已经提交的并发事务读取。可防止脏读,但幻影读和不可重复读仍可能会发生。 |
ISOLATION_REPEATABLE_READ | 对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生。 |
ISOLATION_SERIALIZABLE | 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。 |
四.参考资料
https://www.jianshu.com/p/aa35c8703d61