数据库事务是企业应用最为重要的内容之一。首先讨论Spring数据库的事务应用,然后讨论Spring中最著名的注解之一——@Transactional。
在Spring中数据库事务是通过PlatformTransactionManager进行管理的,而能够支持事务的是org.springframework.transaction.support.TransactionTemplate模板,它是Spring所提供的事务管理器的模板,源码如下:
//事务管理器
private PlatformTransactionManager transactionManager;
......
@Override
public <T> T execute(TransactionCallback<T> action) throws TransactionException{
// 使用自定义的事务管理器
if(this.transactionManager instanceof CallbackPreferringPlatformTransactionManager){
return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this,action);
}else{//系统默认管理器
//获取事务状态
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try{
//回调接口方法
result = action.doInTransaction(status);
}catch(RuntimeException ex){
//Transactional code threw application exception -> rollback
//回滚异常方法
rollbackOnException(status, ex);
//抛出异常
throw ex;
}catch(Error ex){
//Transactional code threw error -> rollback
//回滚异常方法
rollbackOnException(status, err);
//抛出错误
throw err;
}catch(Throwable ex){
//Transactional code threw unexpected exception -> rollback
//回滚异常方法
rollbackOnException(status, ex);
//抛出无法捕获异常
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
//提交事务
this.transactionManager.commit(status);
//返回结果
return result;
}
}
通过上面的源码,可以看到:
public interface PlatformTransactionManager{
//获取事务状态
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
//回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
对于MyBatis框架,用得最多的事务管理器是DataSourceTransactionManager(org.springframework.jdbc.datasource.DataSourceTransactionManager);如果是Hibernate框架,那么要用到spring-orm包org.springframework.orm.hibernate4.HibernateTransactionManager了。它们大同小异,一般而言,在使用时,还会加入XML的事务命名空间。如下所示:
<?xml version='1.0' encoding='UTF-8' ?>
<!-- was: <?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:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 数据库连接池 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/chapter13"/>
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="maxActive" value="255" />
<property name="maxIdle" value="5" />
<property name="maxWait" value="10000" />
</bean>
<!-- 事务管理器配置数据源事务 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
这里先引入了XML的命名空间,然后定义了数据库连接池,于是使用了DataSourceTransactionManager去定义数据库事务管理器,并且注入了数据库连接池。这样Spring就知道已经将数据库事务委托给事务管理器transactionManager管理了。
在Spring中可以使用声明式事务或者编程式事务,编程式事务几乎不用,因此主要介绍声明式事务。声明式事务又可以分为XML配置和注解事务,但是XML方式也已经不用了。目前主流方法是注解@Transactional。
用Java配置的方式来实现Spring数据库事务,需要在配置类中实现接口TransactionManagerConfigurer的annotationDrivenTransactionManager方法。Spring会把annotationDrivenTransactionManager方式返回的事务管理器作为程序中的事务管理器。下面是使用Java配置方式实现Spring的数据库事务配置代码:
/********* import ************/
@Configuration
@ComponentScan("com.ssm.chapter13.*")
//使用事务驱动管理器
@EnableTransactionManagement
public class JavaConfig implements TransactionManagerConfigurer{
//数据源
private DataSource dataSource = null;
/**
*配置数据源
*@return 数据源.
*/
@Bean(name="dataSource")
public DataSource initDataSource(){
if(dataSource != null){
return dataSource;
}
Properties props = new Properties();
props.setProperty("driverClassName","com.mysql.jdbc.Driver");
props.setProperty("url","jdbc:mysql://lcalhost:3306/chapter15");
props.setProperty("username","root");
props.setProperty("password","123456");
props.setProperty("maxActiv","200");
props.setProperty("maxIdle","20");
props.setProperty("maxWait","30000");
try{
dataSource = BasicDataSourceFactory.createDataSource(props);
}catch(Exception e){
e.printStackTrace();
}
return dataSource;
}
/**
*配置jdbcTemplate
*@return jdbcTemplate
*/
@Bean(name="jdbcTemplate")
public JdbcTemplate initjdbcTemplate(){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemlate.setDataSource(initDataSource());
return jdbcTemplate;
}
/**
*实现接口方法,使得返回数据库事务管理器
*/
@Override
@Bean(name = "transactionManager")
public PlatformTransactionManager annotationDriverTransactionManager(){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
//设置事务管理器管理的数据源
transactionManager.setDataSource(initDataSource());
return transactionManager;
}
}
注意,使用注解@EnableTransactionManagement后,在Spring上下文中使用事务注解@Transactional,Spring就会知道使用这个数据库事务管理器管理事务了。
编程式事务以代码的方式管理事务,需要使用一个事务定义接口——TransactionDefinition。只要使用默认的实现类——DefaultTransactionDefinition就可以了。示例代码如下:
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
JdbcTemplate jdbcTemplate = ctx.getBean(JdbcTemplate.clsss);
//事务定义类
TransactionDefinition def = new DefaultTransactionDefinition();
PlatformTransactionManager transactionManager = ctx.getBean(PlatformTransactionManager.class);
TransactionStatus status = transactionManager.getTransaction(def);
try{
//执行SQL语句
jdbcTemplate.update("insert into t_role(role_name, note) values('role_name_transactionManagert','note_transactionManager')");
//提交事务
transactionManager.commit(status);
//回滚事务
transactionManager.rollback(status);
}
所有的事务都是由开发者自己进行控制的,由于事务已交由事务管理器管理,所以JbdcTemplate本身的数据库资源已经由事务管理器管理,因此当它执行完insert语句时不会自动提交事务,这个时候需要使用事务管理器的commit方法,回滚事务需要使用rollback方法。
声明式事务是一种约定型的事务,在大部分情况下,当使用数据库事务时,大部分的场景是在代码中发生了异常时,需要回滚事务,而不发生异常时则提交事务,从而保证数据库数据的一致性。
首先声明式事务允许自定义事务接口——TransactionDefinition,它可以由XML或者注解@Transactional进行配置。
Transactional的源码如下:
package org.springframework.transaction.annotation;
import java.lang.annotation.Documented;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @Interface Transactional{
@AliasFor("transactionManager");
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<?extends Throwable>[] rollbackFor() default{};
String[] rollbackForClassName() default{};
Class<? extends Throwable>[] noRollbackFor() default{};
String[] noRollbackForClassName() default {};
}
下面是它的配置项:
配置项 | 含义 | 备注 |
---|---|---|
value | 定义事务管理器 | 它是Spring IoC容器里的一个Bean id,这个Bean需要实现接口PlatformTransactionManager |
transactionManager | 同上 | 同上 |
isolation | 隔离级别 | 这是一个数据库在多个事务同时存在时的概念,默认值取数据库默认隔离级别 |
propagation | 传播行为 | 传播行为是方法之间调用的问题,默认值为Propagation.REQUIRED |
timeout | 超时时间 | 单位为秒,当超时时,会引发异常,默认会导致事务回滚 |
readOnly | 是否开启只读事务 | 默认值为false |
rollbackFor | 回滚事务的异常类定义 | 也就是只有当方法产生所定义异常时,才回滚事务,否则就提交事务 |
rollbackForClassName | 回复事务的异常类名定义 | 同rollbackFor,只是使用类名称定义 |
noRollbackFor | 当产生哪些异常不回滚事务 | 当产生所定义异常时,Spring将继续提交事务 |
noRollbackForClassName | 同noRollbackFor | 同noRollbackFor,只是使用类的名称定义 |
isolation和propagation是最重要的内容。上述所有的属性将被会Spring放到事务定义类TransactionDefinition中,事务声明器的配置内容也是以这些为主了。
注意:使用声明式事务需要配置注解驱动,只需要在代码中加入如下配置即可使用@Transactional配置事务了:
<tx:annotation-driven transaction-manager="transactionManager"/>
介绍一种通用的XML声明式事务配置,它需要一个事务拦截器——TransactionInterceptor,可以把拦截器想象成AOP编程,首先配置它,代码如下:
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager" />
<!--配置事务属性-->
<property name="transactionAttributes">
<props>
<!--key代表的是业务方法的正则式匹配,而其内容可以配置各类事务定义参数-->
<prop key="insert*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
<prop key="save*">PROPAGATION_REQUIRED, ISOLATION_READ_UNCOMMITTED</prop>
<prop key="add*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
<prop key="select*"PROPAGATION_REQUIRED,readOnly</prop>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="del*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
<prop key="remove*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
<prop key="update*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
</props>
</property>
</bean>
配置transactionAttributes的内容是需要关注的重点,Spring IoC启动时会解析这些内容,放到事务定义类TransactionDefinition中,再运行时会根据正则式的匹配度决定方法采取哪种策略。这也揭示了声明式事务的底层原理——Spring AOP技术。
展示Spring方法采取的事务策略之外,还需要告诉Spring哪些类要使用事务拦截器进行拦截,为此再配置一个类BeanNameAutoProxyCreator:
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>*ServiceImpl</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>transactionInterceptor</value>
</list>
</property>
</bean>
BeanName属性告诉Spring如何拦截类。然后interceptorName则是定义事务拦截器,这样对应的类和方法就会被事务管理器所拦截了。
下面是事务定义器TransactionDefinition的源码;
package org.springframework.transaction;
import java.sql.Connection;
public interface TransactionDefinition{
//传播行为常量定义(7个)
int PROPAGATION_REQUIRED = 0; //默认传播行为
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPATATION_NEVER = 5;
int PROPATATION_NESTED = 6;
//隔离级别定义(5个)
int ISOLATION_DEFAULT = -1;//默认隔离级别
//隔离级别定义(5个)
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_REPEATABLE_READ;
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
int TIMEOUT_DEFAULT = -1; //-1代表永不超时
//获取传播行为
int getPropagationBehavior();
//获取隔离级别
int getIsolationLevel();
//事务超时时间
int getTimeout();
//是否只读事务
boolean isReadOnly();
//获取事务定义器名称
String getName();
}
以上就是关于事务定义器的内容,除了异常的定义,其他关于事务的定义都可以在这里完成。而对于事务的回滚内容,会以RollbackRuleAttribute和NoRollbackRuleAttribute两个类进行保存。
约定十分重要,首先要理解@Transaction注解或者XML配置。@Transaction注解可以使用在方法或者类上面,在Spring IoC容器初始化时,Spring会读入这个注解或着XML配置的事务信息,并且保存到一个事务定义类里面(TransactionDefinition接口的子类),以备将来使用。当运行时会让Spring拦截注解标注的某一个方法或者类的所有方法,这就是AOP的原理。
首先Spring通过事务管理器(PlatformTransactionManager的子类)创建事务,与此同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上配置。然后启动开发者提供的业务代码,即Spring通过反射的方式调度开发者的业务代码,如果反射的结果是异常的,并且符合事务定义类回滚条件,Spring就会将数据库事务回滚,否则将数据库事务提交,这是Spring自己完成的。
声明式事务的流程如下:
以插入角色代码为例:
@Autowired
private RoleDao roleDao = null;
@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,timeout=3)
public int insertRole(Role role){
return roleDao.insert(role);
}
这里配置了Propagation.REQUIRED的传播行为,意味着当别的方法调度时,如果存在事务就沿用下来,如果不存在事务就开启新的事务,而隔离级别采用默认的,并且设置了超时时间为3秒。这就是Spring AOP技术的神奇之处,其底层的实现原理是动态代理,也就是只有代理对象相互调用才能像AOP那么神奇。
数据库事务正确执行的4个基础要素是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
隔离性涉及多个事务并发的状态,会产生数据库丢失更新的问题,其次隔离性又分为多个层级。
在互联网中存在着抢购、秒杀等高并发场景、使得数据库在一个多事务的环境中运行,多个事务的并发会产生一系列的问题,主要的问题之一就是丢失更新,一般而言存在两类丢失更新。
第一类丢失更新:两个事务并发,其中一个回滚,另一个提交成功导致不一致;
第二类丢失更新:两个都提交了的事务,由于在不同的事务中,无法探知其他事务的操作,导致实际结果不符。
目前大部分数据库基本已经消灭了第一类丢失更新,对于第二类丢失更新,主要目标是克服事务之间协助的一致性,方法是在数据库标准规范中定义事务之间的隔离级别,来在不同程度上减少出现丢失更新的可能性。
隔离级别可以在不同程度上减少丢失更新。
按照SQL的标准规范,把隔离级别定义为4层,分别是:脏读(dirty read)、读/写提交(read commit)、可重复度(repeatable read)和序列化(Serializable)。
选择隔离级别的出发点在于两点:性能和数据一致性
一般而言,从脏读到序列化,系统性能直线下降。因此设置高的级别,比如序列化,会严重压制并发,从而引发大量的线程挂起,直到获得锁才能进一步操作,而恢复时又需要大量的等待时间。在大部分场景下,企业会选择读/写提交的方式设置事务,这样既有助于提高并发,又压制了脏读,但是对于数据一致性问题并没有解决。对于一般的应用都可以使用@Transactional方法进行配置,示例如下:
@Autowired
private RoleDao roleDao = null;
//设置方法为读/写提交的隔离级别
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
return roleDao.insert(role);
}
当业务并发量不是很大或者根本不需要考虑的情况下,使用序列化隔离级别用以保证数据的一致性,也是合理的。
总之,隔离级别需要根据并发的大小和性能来做出决定,对于并发不大又需要保证数据安全性的可以使用序列化的隔离级别,这样就能保证数据库在多事务环境中的一致性。代码如下:
@Autowired
private RoleDao roleDao = null;
// 设置方法为序列化的隔离级别
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
public int insertRole(Role role) {
return roleDao.insert(role);
}
注意,实际工作中,注解@Transactional隔离级别的默认值为Isolation.DEFAULT,其含义是默认的,随数据库默认值的变化而变化。不同的数据库隔离级别的支持是不一样的,MySQL支持4种隔离级别,默认是可重复读,而Oracle只支持读/写提交和序列化两种隔离级别,默认为读/写提交。
传播行为是指方法之间的调用事务策略的问题。当一个方法调度另外一个方法时,可以对事务的特性进行传播配置,称之为传播行为。
在Spring中传播行为的类型,是通过一个枚举类型去定义的,这个枚举类是org.springframework.transaction.annotation.Propagation,它定义了7种传播行为,如下表所示:
传播行为 | 含义 | 备注 |
---|---|---|
REQUIRED | 当方法调用时,如果不存在当前事务,那么就创建事务;如果之前的方法已经存在事务了,那么就沿用之前的事务 | 这是Spring默认的传播行为 |
SUPPORTS | 当方法调用时,如果不存在当前事务,那么不启用事务;如果存在当前事务,那么就沿用当前事务 | – |
MANDATORY | 方法必须在事务内允许 | 如果不存在当前事务,那么就抛出异常 |
REQUIRES_NEW | 无论是否存在当前事务,方法都会在新的事务中允许 | 也就是事务管理器会打开新的事务运行该方法 |
NOT_SUPPORTED | 不支持事务,如果不存在当前事务也不会创建事务;如果存在当前事务,则挂起它,直到该方法结束后才恢复当前事务 | 适用于那些不需要事务的SQL |
NEVER | 不支持事务,只有在没有事务的环境中才能运行它 | 如果方法存在当前事务,则抛出异常 |
NESTED | 嵌套事务,也就是调用方法如果抛出异常只回滚自己内部执行的SQL,而不回滚主方法的SQL | 它的实现存在两种情况,如果当前数据库支持保存点(savepoint),那么它就会在当前事务上使用保存点技术;如果发生异常则将方法内执行的SQL回滚到保存点上,而不是全部回滚,否则就等同于REQUIRES_NEW创建新的事务运行方法代码 |
最常用的时REQUIRED,也是默认的传播行为。一般而言,企业比较关注的时REQUIRES_NEW和NESTED.
通过Spring和MyBatis的组合,给出一个较为详细的实例:
目录图如下:
文件作用表如下:
首先,搭建环境。环境配置的XML文件如下:
<?xml version='1.0' encoding='UTF-8' ?>
<!-- was: <?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:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!--启用扫描机制,并指定扫描对应的包-->
<context:annotation-config />
<context:component-scan base-package="com.ssm.chapter13.*" />
<!-- 数据库连接池 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/chapter13"/>
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="maxActive" value="255" />
<property name="maxIdle" value="5" />
<property name="maxWait" value="10000" />
</bean>
<!-- 集成MyBatis -->
<bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--指定MyBatis配置文件-->
<property name="configLocation" value="classpath:/mybatis/mybatis-config.xml" />
</bean>
<!-- 事务管理器配置数据源事务 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 使用注解定义事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
<!-- 采用自动扫描方式创建mapper bean -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ssm.chapter13" />
<property name="SqlSessionFactory" ref="SqlSessionFactory" />
<property name="annotationClass" value="org.springframework.stereotype.Repository" />
</bean>
</beans>
先给出数据库表映射的POJO类:
public class Role {
private Long id;
private String roleName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
搭建MyBatis的映射文件,建立SQL和POJO的关系:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ssm.chapter13.mapper.RoleMapper">
<insert id="insertRole" parameterType="com.ssm.chapter13.pojo.Role">
insert into t_role (role_name, note) values(#{roleName}, #{note})
</insert>
</mapper>
配置一个接口:
import com.ssm.chapter13.pojo.Role;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleMapper {
public int insertRole(Role role);
}
为了引入这个映射器,需要配置一个MyBatis的配置文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<mappers>
<mapper resource="com/ssm/chapter13/sqlMapper/RoleMapper.xml"/>
</mappers>
</configuration>
到此MyBatis部分的内容已经配置完成,接着配置一些服务(Service)类。对于服务类,坚持“接口+实现类“的规则,先定义两个接口:
import java.util.List;
import com.ssm.chapter13.pojo.Role;
public interface RoleService {
public int insertRole(Role role);
}
import java.util.List;
import com.ssm.chapter13.pojo.Role;
public interface RoleListService {
public int insertRoleList(List<Role> roleList);
}
RoleService接口的insertRole方法可以对单个角色进行插入,而RoleListService的insertRoleList方法可以对角色列表进行插入。注意insertRoleList方法会调用insertRole,这样就可以测试各类的传播行为了。下面是两个接口的实现类:
import java.util.List;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleListService;
import com.ssm.chapter13.service.RoleService;
@Service
public class RoleListServiceImpl implements RoleListService {
@Autowired
private RoleService roleService = null;
Logger log = Logger.getLogger(RoleListServiceImpl.class);
@Override
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public int insertRoleList(List<Role> roleList) {
int count = 0;
for (Role role : roleList) {
try {
count += roleService.insertRole(role);
} catch (Exception ex) {
log.info(ex);
}
}
return count;
}
}
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ssm.chapter13.mapper.RoleMapper;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleService;
@Service
public class RoleServiceImpl implements RoleService, ApplicationContextAware {
@Autowired
private RoleMapper roleMapper = null;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
return roleMapper.insertRole(role);
}
}
两个服务实现类方法标注了@Transactinal注解,它们都会以对应的隔离级和传播行为中运行。
为了更好地测试从而输出对应的日志,修改log4j的配置文件:
log4j.rootLogger=DEBUG , stdout
log4j.logger.org.springframework=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n
测试代码如下:
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleListService;
public class Chapter13Main {
public static void main(String [] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext ("spring-cfg.xml");
RoleListService roleListService = ctx.getBean(RoleListService. class);
List<Role> roleList = new ArrayList<Role>();
for (int i=1; i<=2; i++) {
Role role = new Role();
role.setRoleName("role_name_" + i);
role.setNote("note_" + i);
roleList.add(role);
}
int count = roleListService.insertRoleList(roleList);
System.out.println(count);
}
}
这里插入了两个角色,由于insertRoleList会调用insertRole,而insertRole标注了REQUIRES_NEW,所以每次调用会产生新的事务。
由于保存点技术并不是每一个数据库都能支持的,所以当你把传播行为设置为NESTED时,Spring会先去探测当前数据库是否能够支持保存点技术。如果数据库不予支持,它就会和REQUIRES_NEW一样创建新事务去运行代码,以达到内部方法发生异常时并不回滚当前事务的目的。
有时候配置了注解@Transactional,但是它会失效,这里要注意一些细节问题,以避免落入陷阱。
注解@Transaction的底层实现是Spring AOP技术,而Spring AOP技术使用的是动态代理。这就意味着对于静态(static)方法和非public方法,注解@Transactional是失效的。还有一个更为隐秘的,而且在使用过程中极其容易犯错误的——自调用。
所谓自调用,就是一个类的一个方法去调用自身另外一个方法的过程。示例代码如下:
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ssm.chapter13.mapper.RoleMapper;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleService;
@Service
public class RoleServiceImpl implements RoleService, ApplicationContextAware {
@Autowired
private RoleMapper roleMapper = null;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
return roleMapper.insertRole(role);
}
//自调用问题
@Override
@Transactional(propagation = Propagation.REQUIRED, isolation=Isolation.READ_COMMITTED)
public int insertRoleList(List<Role> roleList) {
int count = 0;
for (Role role : roleList) {
try {
//调用自身类的方法,产生自调用问题
insertRole(role);
count++;
} catch (Exception ex) {
ex.printStackTrace();
}
}
return count;
}
}
在insertRoleList方法的实现中,它调用了自身类实现insertRole的方法,而insertRole声明是REQUIRES_NEW的传播行为,也就是每次调用就会产生新的事务运行。
出现这个的问题根本原因在于AOP的实现原理。由于@Transactional的实现原理是AOP,而AOP的实现原理是动态代理,而自己调用自己的过程,并不存在代理对象的调用,这样就不会产生AOP去为我们设置@Transactional配置的参数,这样就出现了自调用注解失效的问题。
为了克服这个问题,一方面可以使用两个服务类,Spring IoC容器中为你生成了RoleService的代理对象,这样就可以使用AOP,且不会出现自调用的问题。另外一方面,你也可以直接从容器中获取Service的代理对象,从IoC容器中获取RoleService代理对象。代码如下:
//消除自调用问题
@Override
@Transactional(propagation = Propagation.REQUIRED, isolation= Isolation.READ_COMMITTED)
public int insertRoleList2(List<Role> roleList) {
int count = 0;
//从容器中获取RoleService对象,实际是一个代理对象
RoleService service = ctx.getBean(RoleService.class);
for (Role role : roleList) {
try {
service.insertRole(role);
count++;
} catch (Exception ex) {
ex.printStackTrace();
}
}
return count;
}
从容器获取代理对象的方法克服了自调用的过程,但是有一个弊端,就是从容器获取代理对象的方法有侵入之嫌,你的类需要依赖于Spring IoC容器,而这个类可以使用另一个服务类去调用。
数据事务是企业应用关注的核心内容,也是开发者最容易犯错的问题,因此这里列举一些使用不良习惯,注意它们可以避免一些错误和性能的丢失。
互联网往往采用模型—视图—控制器(Model View Controller,MVC)来搭建开发环境,因此在Controller中使用Service是十分常见的。
在Controller每调用一次带事务的service都会创建数据库事务。如果多次调用,则不在同一个事务中,这会造成不同时提交和回滚不一致的问题。每一个Java EE开发者都要注意这类问题,以避免一些不必要的错误。
在企业的生产系统中,数据库事务资源是最宝贵的资源之一,使用了数据库事务之后,要及时释放数据库事务。换言之,我们应该尽可能地使用数据库事务资源去完成所需工作,但是在一些工作中需要使用到文件、对外连接等操作,而这些操作往往会占用较长时间,针对这些,如果开发者不注意细节,就很容易出现系统宕机的问题。
一些系统之间的通信及一些可能需要花费较长时间的操作,都要注意这个问题,放在controller层等事务外进行处理,以避免长时间占用数据库事务,导致系统性能的低下。
带事务的service中,出现异常想要回滚时应抛出异常,而不是捕获。