在名为Spring_Demo的项目中创建名为“Transaction”的普通Java模块,并在该模块中创建名为“lib”的包用于存放该模块所需的所有jar包。在该开发环境中与前一篇所需的开发环境一致,jar包参考前篇博客《Spring使用篇(十)—— Spring与MyBatis整合》。
在该模块中创建如下图所示的包结构,其中:
config包: 用于存放Java配置类;
controller包: 用于存放控制器类;
mapper包: 用于存放数据库操作接口;
pojo包: 用于存放POJO类;
service包: 用于存放业务接口;
service.impl包: 用于存放业务接口的实现类;
sql包: 用于存放MyBatis框架所需的数据库映射Mapper文件;
test包: 用于存放测试类
database-config.properties属性文件: 用于存储数据库四要素属性值;
log4j.properties: 为日志配置属性文件;
spring-config.xml: 为Spring框架配置文件;
SqlMapConfig.xml: 为MyBatis框架的核心配置文件。
其余开发环境包括数据库的创建,数据表的创建,POJO类的创建,Mapper接口的定义,数据库映射文件的编写以及Java配置类的基础配置,均与上一篇博客(《Spring使用篇(十)—— Spring与MyBatis整合》)相同。
在Spring中数据库事务是通过PlatformTransactionManager接口进行管理的。在Spring中,有多种事务管理器,由于目前在持久层常用MyBatis框架,因此常用的事务管理器是DataSourceTransactionManager(org.springframework.jdbc.datasource.DataSourceTransactionManager),它继承抽象事务管理器AbstractPlatformTransactionManager,而AbstractPlatformTransactionManager又实现了PlatformTransactionManager接口。
因此在Spring的XML配置文件中加入对事务管理器的配置,同时还需要加入XML的事务命名空间,spring-config.xml具体的配置修改如下:
<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:property-placeholder location="database-config.properties" ignore-resource-not-found="true" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.database.driver}" />
<property name="url" value="${jdbc.database.url}" />
<property name="username" value="${jdbc.database.username}" />
<property name="password" value="${jdbc.database.password}" />
<property name="maxActive" value="255" />
<property name="maxIdle" value="5" />
<property name="maxWait" value="10000" />
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:sqlMapConfig.xml" />
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ccff.spring.transaction.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="annotationClass" value="org.springframework.stereotype.Repository" />
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
bean>
beans>
在配置DataSourceTransactionManager事务管理器时注入了数据库连接池,这样Spring就知道此事已经将数据库事务委托给了事务管理器transactionManager管理了。
数据库事务的管理一定会涉及对数据库,尤其是数据库事务的知识,但本系列博客主要是对框架使用的学习,因此在这里只做简单说明。
并行事务的四大问题:第一,更新丢失:和别的事务读到相同的东西,各自写,自己的写被覆盖了。(谁写的快谁的更新就丢失了)。第二,脏读:读到别的事务未提交的数据。(万一回滚,数据就是脏的无效的了)。第三,不可重复读:两次读之间有别的事务修改。第四,幻读:两次读之间有别的事务增删。
数据的隔离级别主要有:读未提交、读已提交、可重复读和可序列化。对应的隔离级别分别为:READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE。各类隔离级别和产生的现象如下表所示:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
可序列化 | 不可能 | 不可能 | 不可能 |
而选取隔离级别的出发点在于两点:性能和数据一致性。数据库的隔离级别从读未提交到可序列化,系统性能直线下降。在实际工作中,注解@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创建新的事务运行方法代码 |
7种传播行为中,最常用的是REQUIRED,也是默认的传播行为。对于那些不支持事务的方法我们使用得不多,一般而言,我们还比较关注的是REQUIRES_NEW和NESTED。
在Spring中可以使用编程式事务与声明式事务。如今,编程式事务几乎不用了,因为它会产生荣誉,代码可读性差,因此这里只介绍声明式事务。声明式事务又可以分为XML配置和注解事务,但XML方式也已经不常用了,目前主流方法是注解@Transactional,因此本篇仅介绍使用注解@Transactional。
声明式事务是一种约定型的事务,在大部分情况下,当使用数据库事务时,大部分的场景是在代码中发生了异常时,需要回滚事务,而不发生异常时则是提交事务,从而保证数据库数据的一致性。从这点出发,Spring给了一个约定,如果使用的是声明式事务,那么当你的业务方法不发生异常(或者发生异常,但该异常也被配置信息允许提交事务)时,Spring就会让事务管理器提交事务,而发生异常(并且该异常不被你的配置信息所允许提交事务)时,则让事务管理器回滚事务。
声明式事务允许自定义事务接口——TransactionDefinition,它可以由XML或者注解@Transactional进行配置,本篇主要讨论注解@Transactional,其配置项如下表所示:
配置项 | 含义 | 备注 |
---|---|---|
value | 定义事务管理器 | 它是Spring IoC容器里的一个Bean id,这个Bean需要实现接口PlatformTransactionManager |
transactionManager | 同上 | 同上 |
isolation | 隔离级别 | 这是一个数据库在多个事务同时存在时的概念,默认值取数据库默认的隔离级别 |
propagation | 传播行为 | 传播行为是方法之间调用的问题,默认值为Propagation.REQUIRED |
timeout | 超时时间 | 单位为秒,当超时时,会引发异常,默认会导致事务回滚 |
readOnly | 是否开启只读事务 | 默认值为false |
rollbackFor | 回滚事务的异常类定义 | 也就是只有当方法产生所定义异常时,才回滚事务,否则就提交事务 |
rollbackForClassName | 回滚事务的异常类名定义 | 同rollbackFor,只是使用类名称定义 |
noRollbackFor | 当产生哪些异常不回滚事务 | 当产生所定义的异常时,Spring会继续提交事务 |
noRollbackForClassName | 同noRollbackFor | 同noRollbackFor,只是使用类的名称定义 |
当了解了注解@Transactional的配置项后,有两种方式开启该注解。第一种是在Spring的配置XML文件spring-config.xml中通过如下配置开启,就可以使用注解@Transactional配置事务了,具体修改如下:
<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:property-placeholder location="database-config.properties" ignore-resource-not-found="true" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.database.driver}" />
<property name="url" value="${jdbc.database.url}" />
<property name="username" value="${jdbc.database.username}" />
<property name="password" value="${jdbc.database.password}" />
<property name="maxActive" value="255" />
<property name="maxIdle" value="5" />
<property name="maxWait" value="10000" />
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:sqlMapConfig.xml" />
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ccff.spring.transaction.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="annotationClass" value="org.springframework.stereotype.Repository" />
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
bean>
<tx:annotation-driven transaction-manager="transactionManager" />
beans>
第二种方式是在Java Config配置类中通过注解@EnableTransactionManagement,使用事务驱动管理器。即修改config包下的JavaConfig配置类,具体代码如下:
package com.ccff.spring.transaction.config;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleService;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan(basePackageClasses = {Role.class})
@ImportResource({"spring-config.xml"})
@EnableTransactionManagement
public class JavaConfig{
}
在声明式事务中,约定十分重要。注解@Transactional可以使用在方法或者类上面,在Spring IoC容器初始化时,Spring会读入这个注解的事务信息,并且保存到一个事务定义类里面(TransactionDefinition接口的子类),以备将来使用。当运行时会让Spring拦截注解标注的某一个方法或者类的所有方法。
首先Spring通过事务管理器(PlatformTransactionManager的子类)创建事务,榆次同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上设置。而根据传播行为配置采取一种特定的策略,这是Spring根据配置完成的内容,作为你开发者的我们只需要配置,无须编码。
然后启动开发者提供的业务代码,此时Spring会通过反射的方式调度开发者的业务代码,但是反射的结果可能是正常返回或者产生异常返回,那么它给定的约定是只要发生异常,并且符合事务定义类回滚条件的,Spring就会将数据库事务回滚,否则将数据库事务提交,这也是Spring自己完成的。
至此,我们可以发现,在Spring的整个事务管理过程中,我们只需要编写业务代码和对事务属性进行配置就可以了,并不需要使用代码对事务的管理进行干预,工作量变少,代码逻辑也更清晰,更利于维护。这样Spring就通过Spring AOP技术使得我们可以把精力放在业务的开发上,而不是控制数据库的资源和事务上。但同时我们也必须清楚,Spring AOP的底层实现原理是动态代理,也就是只有代理对象相互调用才能像AOP那么神奇,在实际开发过程中还是有很多陷阱的。
在前面的小节中,已经在本实验开发测试环境中配置了Spring的配置文件,整合了MyBatis框架,配置了事务管理器,并在Java Config配置类中开启了注解@Transactional。在此直接列出所有相关配置文件。
第一步,Spring配置文件spring-config.xml配置如下:
<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:property-placeholder location="database-config.properties" ignore-resource-not-found="true" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.database.driver}" />
<property name="url" value="${jdbc.database.url}" />
<property name="username" value="${jdbc.database.username}" />
<property name="password" value="${jdbc.database.password}" />
<property name="maxActive" value="255" />
<property name="maxIdle" value="5" />
<property name="maxWait" value="10000" />
bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:sqlMapConfig.xml" />
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.ccff.spring.transaction.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
<property name="annotationClass" value="org.springframework.stereotype.Repository" />
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
bean>
beans>
第二步,pojo包下的Role类代码如下所示:
package com.ccff.spring.transaction.pojo;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Role {
private Long id;
private String roleName;
private String roleNote;
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 getRoleNote() {
return roleNote;
}
public void setRoleNote(String roleNote) {
this.roleNote = roleNote;
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", roleName='" + roleName + '\'' +
", roleNote='" + roleNote + '\'' +
'}';
}
}
第三步,mapper包下的RoleMapper接口如下所示:
package com.ccff.spring.transaction.mapper;
import com.ccff.spring.transaction.pojo.Role;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleMapper {
public int insertRole(Role role);
public Role getRoleById(Long id);
public int updateRole(Role role);
public int deleteRole(Long id);
}
第四步,sql包下的RoleMapper.xml配置如下:
<mapper namespace="com.ccff.spring.transaction.mapper.RoleMapper">
<insert id="insertRole" parameterType="Role" useGeneratedKeys="true" keyProperty="id">
insert into t_role(roleName, roleNote) values (#{roleName}, #{roleNote})
insert>
<delete id="deleteRole" parameterType="long">
delete from t_role where id=#{id}
delete>
<select id="getRoleById" parameterType="long" resultType="Role">
select id, roleName, roleNote from t_role where id = #{id}
select>
<update id="updateRole" parameterType="Role">
update t_role
set roleName = #{roleName},
roleName = #{roleName}
where id = #{id}
update>
mapper>
第五步,MyBatis全局配置文件sqlMapConfig.xml的配置如下所示:
<configuration>
<settings>
<setting name="cacheEnabled" value="true" />
<setting name="useGeneratedKeys" value="true" />
<setting name="defaultExecutorType" value="REUSE" />
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="defaultStatementTimeout" value="25000"/>
settings>
<typeAliases>
<typeAlias alias="Role" type="com.ccff.spring.transaction.pojo.Role" />
typeAliases>
<mappers>
<mapper resource="com/ccff/spring/transaction/sql/RoleMapper.xml" />
mappers>
configuration>
第六步,在service包下创建名为“RoleService”的接口,该接口的insertRole方法可以对单个角色进行插入,具体代码如下所示:
package com.ccff.spring.transaction.service;
import com.ccff.spring.transaction.pojo.Role;
public interface RoleService {
public int insertRole(Role role);
}
在service包下创建名为“RoleListService”的接口,该接口的insertListRole方法可以对角色列表进行插入,而在该方法中会调用insertRole,具体代码如下所示:
package com.ccff.spring.transaction.service;
import com.ccff.spring.transaction.pojo.Role;
import java.util.List;
public interface RoleListService {
public int insertListRole(List<Role> roleList);
}
在service.impl包下创建名为“RoleServiceImpl”的类并实现接口RoleService,具体代码如下所示:
package com.ccff.spring.transaction.service.impl;
import com.ccff.spring.transaction.mapper.RoleMapper;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Component
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleMapper roleMapper = null;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
return roleMapper.insertRole(role);
}
}
在service.impl包下创建名为“RoleListServiceImpl”的类并实现接口RoleListService,具体代码如下所示:
package com.ccff.spring.transaction.service.impl;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleListService;
import com.ccff.spring.transaction.service.RoleService;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Component
public class RoleListServiceImpl implements RoleListService {
@Autowired
private RoleService roleService = null;
Logger logger = Logger.getLogger(RoleListServiceImpl.class);
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
public int insertListRole(List<Role> roleList) {
int count = 0;
for (Role role : roleList){
try{
count += roleService.insertRole(role);
}catch (Exception ex){
logger.info(ex);
}
}
return count;
}
}
在这两个接口的实现类中,均标注了注解@Transactional,这样它们都会在对应的隔离级别和传播行为中运行。由于insertRole方法采用了Propagation.REQUIER_NEW的传播行为,因此每当insertRoleList方法调度了insertRole方法时,就会产生一个新的事务。
第七步,在config包中的JavaConfig配置类中对Role类与service服务接口进行配置,具体代码如下所示:
package com.ccff.spring.transaction.config;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleService;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan(basePackageClasses = {Role.class,RoleService.class})
@ImportResource({"spring-config.xml"})
@EnableTransactionManagement
public class JavaConfig{
}
第八步,在test包下创建名为“RoleTest”的实现类,具体代码如下所示:
package com.ccff.spring.transaction.test;
import com.ccff.spring.transaction.config.JavaConfig;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleListService;
import com.ccff.spring.transaction.service.RoleService;
import com.ccff.spring.transaction.service.RoleService2;
import com.ccff.spring.transaction.service.impl.RoleServiceImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.ArrayList;
import java.util.List;
public class RoleTest {
@Test
public void test1(){
ApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
RoleListService roleListService = context.getBean(RoleListService.class);
List<Role> roleList = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
Role role = context.getBean(Role.class);
role.setId(Long.parseLong(i+""));
role.setRoleName("role-name-"+i);
role.setRoleNote("role-note-"+i);
roleList.add(role);
}
int count = roleListService.insertListRole(roleList);
System.out.println(count);
}
}
在上面的代码中,插入了三个角色,由于insertRoleList会调用insertRole,而insertRole标注了REQUIRE_NEW,所以每次调用会产生新的事务。
注解@Transactional的底层实现是Spring AOP技术,而Spring AOP技术使用的是动态代理。这就意味着对于静态(static)方法和非public方法,注解@Transactional是失效的。还有一个更为隐秘的情况,而且在使用过程中极其容易犯错误的——自调用。所谓自调用就是一个类的一个方法去调用自身另一个方法的过程。
在service包下创建名为“RoleService2”的接口,该接口具体代码如下:
package com.ccff.spring.transaction.service;
import com.ccff.spring.transaction.pojo.Role;
import java.util.List;
public interface RoleService2 {
public int insertRole(Role role);
public int insertListRole(List<Role> roleList);
}
在service.impl包下创建该接口的实现类“RoleService2Impl”,具体代码如下所示:
package com.ccff.spring.transaction.service.impl;
import com.ccff.spring.transaction.config.JavaConfig;
import com.ccff.spring.transaction.mapper.RoleMapper;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleService2;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Component
public class RoleService2Impl implements RoleService2 {
@Autowired
private RoleMapper roleMapper = null;
Logger logger = Logger.getLogger(RoleService2Impl.class);
@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 insertListRole(List<Role> roleList) {
int count = 0;
for (Role role : roleList){
try{
//调用自身类的方法,产生自调用问题
insertRole(role);
count++;
}catch (Exception ex){
logger.info(ex);
}
}
return count;
}
}
在test包下的RoleTest测试类中创建名为“test2”的方法,具体代码如下所示:
package com.ccff.spring.transaction.test;
import com.ccff.spring.transaction.config.JavaConfig;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleListService;
import com.ccff.spring.transaction.service.RoleService;
import com.ccff.spring.transaction.service.RoleService2;
import com.ccff.spring.transaction.service.impl.RoleServiceImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.ArrayList;
import java.util.List;
public class RoleTest {
@Test
public void test1(){
ApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
RoleListService roleListService = context.getBean(RoleListService.class);
List<Role> roleList = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
Role role = context.getBean(Role.class);
role.setId(Long.parseLong(i+""));
role.setRoleName("role-name-"+i);
role.setRoleNote("role-note-"+i);
roleList.add(role);
}
int count = roleListService.insertListRole(roleList);
System.out.println(count);
}
@Test
public void test2(){
ApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
RoleService2 roleService2 = context.getBean(RoleService2.class);
List<Role> roleList = new ArrayList<>();
for (int i = 4; i <= 6; i++) {
Role role = context.getBean(Role.class);
role.setRoleName("role-name-"+i);
role.setRoleNote("role-note-"+i);
roleList.add(role);
}
int count = roleService2.insertListRole(roleList);
System.out.println(count);
}
}
运行test2方法查看日志信息可知,角色插入每次都使用了同一个事务,也就是说,在insertRole方法上标注的@Transactional失效了。
出现这个问题根本原因在AOP的实现原理。由于@Transactional的实现原理是AOP,而AOP的实现原理是动态代理,而在RoleService2Impl中使用的是自己调用自己的过程。换句话说,并不存在代理对象的调用,这样就不会产生AOP去为我们设置@Transactional配置的参数,这样就出现了自调用注解失效的问题。
为了克服这个问题,一方面可以像第五小节演示的那样使用两个接口,Spring IoC容器中生成了RoleService的代理对象,这样就可以使用AOP,且不会出现自调用的问题。但很明显的是把它们整合在一个接口中可能会更加符合逻辑,因此我们也可以直接从容器中获取RoleService的代理对象,从IoC容器中获取RoleService对象,具体修改RoleService2Impl类的代码为:
package com.ccff.spring.transaction.service.impl;
import com.ccff.spring.transaction.config.JavaConfig;
import com.ccff.spring.transaction.mapper.RoleMapper;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleService2;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Component
public class RoleService2Impl implements RoleService2 {
@Autowired
private RoleMapper roleMapper = null;
Logger logger = Logger.getLogger(RoleService2Impl.class);
@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 insertListRole(List<Role> roleList) {
int count = 0;
ApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
RoleService2 roleService2 = context.getBean(RoleService2.class);
for (Role role : roleList){
try{
count += roleService2.insertRole(role);
}catch (Exception ex){
logger.info(ex);
}
}
return count;
}
}
从容器中获取代理对象的方法可以克服自调用的过程,但是有一个弊端,就是从容器获取代理对象的方法有侵入之嫌,你的类需要依赖于Spring IoC容器,而这个问题也可以通过两个服务类去调用来解决。
互联网普遍采用MVC来搭建开发环境,因此在Controller中使用Service是十分常见的。因此我们在controller包中创建名为“RoleController”的控制器类,在该类中调用RoleService接口提供的方法插入两个角色信息,并在两个角色需要在同一个事务中处理,则RoleController类的代码如下所示:
package com.ccff.spring.transaction.controller;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleListService;
import com.ccff.spring.transaction.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class RoleController {
@Autowired
private RoleService roleService = null;
@Autowired
private RoleListService roleListService = null;
public void errerUseServices(){
Role role1 = new Role();
role1.setRoleName("role1-name");
role1.setRoleNote("role1-note");
roleService.insertRole(role1);
Role role2 = new Role();
role2.setRoleName("role2-name");
role2.setRoleNote("role2-note");
roleService.insertRole(role2);
}
}
类似这样的代码在工作中常常出现,这里存在的问题是两个insertRole方法根本不在同一个事务里的问题。
当一个Controller使用Service方法时,如果这个Service标注有注解@Transactional,那么它就会启用一个事务,而一个Service方法完成后,它就会释放该事务,所以前后两个insertRole方法是在两个不同的事务中完成的。
这个例子明确告诉大家使用带有事务的Service,当调用时,如果不是调用Service方法,Spring会为你创建对应的数据库事务。如果多次调用,则不在同一个事务中,这就会造成不同时提交和回滚不一致问题。
在企业的生产系统中,数据库事务资源是最宝贵的资源之一,使用了数据库事务之后,要及时释放数据库事务。换言之,我们应该尽可能地使用数据库事务资源去完成所需工作,但是在一些工作中需要使用到文件、对外连接等操作,而这些操作往往会占用较长时间,针对这些,如果我们作为开发者而不注意这些细节时,就很容易出现系统宕机问题。
假设在插入角色后还需要操作一个文件,于是我们要改造RoleServiceImpl实现类的insertRole方法如下所示:
package com.ccff.spring.transaction.service.impl;
import com.ccff.spring.transaction.mapper.RoleMapper;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Component
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleMapper roleMapper = null;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
int count = roleMapper.insertRole(role);
//操作一些与数据库无关的操作
doSomethingForFile();
return count;
}
}
假设doSomethingForFile方法是一个与数据库事务无关的操作,比如处理图片的上传,这是一段糟糕的代码。当insertRole方法结束后,Spring才会释放数据库事务资源,也就是说在运行doSomethingForFile方法时,Spring并没有释放数据库事务资源,而等到doSomethingForFile方法运行完成后,返回count后才会关闭数据库资源。
在大型互联网系统中,一个数据库的链接可能仅有50+条,然后同时并发的请求则可能是成百上千条。对于这些请求,大部分的并发请求都在等待50条占有数据库连接资源的文件操作了,假如平均一个doSomethingForFile的操作需要1秒,对于同时出现1000条并发请求的网站,就会出现请求卡顿的状态。因为大部分的请求都在等待数据库事务资源的分配,显然这是一个糟糕的结果。
解决该问题的方法就是将doSomethingForFile文件操作方法放到Controller中。在RoleController中添加addRole方法,具体代码如下:
package com.ccff.spring.transaction.controller;
import com.ccff.spring.transaction.pojo.Role;
import com.ccff.spring.transaction.service.RoleListService;
import com.ccff.spring.transaction.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class RoleController {
@Autowired
private RoleService roleService = null;
@Autowired
private RoleListService roleListService = null;
public void errerUseServices(){
Role role1 = new Role();
role1.setRoleName("role1-name");
role1.setRoleNote("role1-note");
roleService.insertRole(role1);
Role role2 = new Role();
role2.setRoleName("role2-name");
role2.setRoleNote("role2-note");
roleService.insertRole(role2);
}
@RequestMapping("/addRole")
@ResponseBody
public Role addRole(Role role){
roleService.insertRole(role);
//操作一些与数据库无关的操作
doSomethingForFile();
return role;
}
}
这样当程序运行完insertRole方法后,Spring会释放数据库事务资源,而不再占用。对于doSomethingForFile方法而言,已经在一个没有事务的环境中运行了,这样当前的请求就不会长期占用数据库事务资源,使得其他并发的请求被迫等待其释放了。其实不仅仅是文件操作,还有一系列系统之间的通信以及一些可能需要花费较长时间的操作,都要注意这个问题,避免长时间占用数据库事务,导致系统性能的低下。
模拟一段购买商品的代码,其中ProductService是产品服务类,而TransactionService是记录交易信息,需求显然就是产品减库存和保存交易在同一个事务里面,要么同时成功,要么同时失败,并且假设减库存和保存交易的传播行为都为REQUIRED,具体代码如下:
@Autowired
private ProductServcie productServcie;
@Autowired
private TransactionService transactionService;
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
public int doTransaction(TransactionBean trans){
int result = 0;
try{
//减少库存
int result = productServcie.decreaseStock(trans.getProductId, trans.getQuantity());
//如果减少库存成功则保存记录
if(result > 0){
transactionService.save(trans);
}
}catch(Exception ex){
//自行处理异常代码
//记录异常日志
log.info(ex);
}
return result;
}
这里的问题是方法已经存在异常了,由于开发者不了解Spring的事务约定,在两个操作的方法里面加入了自己的try/catch语句,就可能发生这样的结果:当减少库存成功了,但是保存交易信息时失败而发生了异常,此时由于开发者加入了try/catch语句,所以Spring在数据库事务所约定的流程中再也得不到任何异常信息了,此时Spring就会提交事务,这样就出现了库存减少,而交易记录却没有保存的糟糕情况。在那些需要大量异常处理的代码中,我们需要小心这样的问题,因此需要对上面的代码进行如下改进:
@Autowired
private ProductServcie productServcie;
@Autowired
private TransactionService transactionService;
@Override
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED)
public int doTransaction(TransactionBean trans){
int result = 0;
try{
//减少库存
int result = productServcie.decreaseStock(trans.getProductId, trans.getQuantity());
//如果减少库存成功则保存记录
if(result > 0){
transactionService.save(trans);
}
}catch(Exception ex){
//自行处理异常代码
//记录异常日志
log.info(ex);
//自行抛出异常,让Spring事务管理流程获取异常,进行事务管理
throw new RuntimeException(ex);
}
return result;
}
上面的代码中自行抛出了一个运行异常,这样在Spring的事务流程中,就会捕捉到抛出的这个异常,进行事务回滚,从而保证了产品减库存和交易记录保存的一致性,这样才是正确的用法,使用事务时要时刻记住Spring和我们的约定流程。