Spring使用篇(十一)—— Spring与MyBatis事务管理

文章目录

  • 1、搭建开发测试环境
  • 2、配置Spring数据库事务
  • 3、数据库隔离级别与传播行为
    • 3.1 数据库隔离级别
    • 3.2 Spring的7种传播行为
  • 4、声明式事务
    • 4.1 声明式事务概述
    • 4.2 注解@Transactional配置项
    • 4.3 声明式事务的约定流程
  • 5、在Spring+MyBatis组合中使用事务
  • 6、注解@Transactional的自调用失效问题
    • 6.1 问题出错演示
    • 6.2 问题解决办法
  • 7、典型错误用法剖析
    • 7.1 错误使用Service
    • 7.2 过长时间占用事务
    • 7.3 错误捕获异常

1、搭建开发测试环境

  在名为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框架的核心配置文件。
Spring使用篇(十一)—— Spring与MyBatis事务管理_第1张图片
  其余开发环境包括数据库的创建,数据表的创建,POJO类的创建,Mapper接口的定义,数据库映射文件的编写以及Java配置类的基础配置,均与上一篇博客(《Spring使用篇(十)—— Spring与MyBatis整合》)相同。

2、配置Spring数据库事务

  在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管理了。

3、数据库隔离级别与传播行为

  数据库事务的管理一定会涉及对数据库,尤其是数据库事务的知识,但本系列博客主要是对框架使用的学习,因此在这里只做简单说明。

3.1 数据库隔离级别

  并行事务的四大问题:第一,更新丢失:和别的事务读到相同的东西,各自写,自己的写被覆盖了。(谁写的快谁的更新就丢失了)。第二,脏读:读到别的事务未提交的数据。(万一回滚,数据就是脏的无效的了)。第三,不可重复读:两次读之间有别的事务修改。第四,幻读:两次读之间有别的事务增删。

  数据的隔离级别主要有:读未提交、读已提交、可重复读和可序列化。对应的隔离级别分别为:READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE。各类隔离级别和产生的现象如下表所示:

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可序列化 不可能 不可能 不可能

  而选取隔离级别的出发点在于两点:性能和数据一致性。数据库的隔离级别从读未提交到可序列化,系统性能直线下降。在实际工作中,注解@Transactional隔离级别的默认值为Isolation.DEFAULT,其含义是默认的,随数据库默认值的变化而变化。因为对不同的数据库而言,隔离级别的支持是不一样的。在MySQL中可支持4种隔离级别,而默认的是可重复读的隔离级别。而在Oracle中只支持读已提交和可序列化两种隔离级别,默认值是读已提交。

3.2 Spring的7种传播行为

  在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。

4、声明式事务

4.1 声明式事务概述

  在Spring中可以使用编程式事务与声明式事务。如今,编程式事务几乎不用了,因为它会产生荣誉,代码可读性差,因此这里只介绍声明式事务。声明式事务又可以分为XML配置和注解事务,但XML方式也已经不常用了,目前主流方法是注解@Transactional,因此本篇仅介绍使用注解@Transactional。

  声明式事务是一种约定型的事务,在大部分情况下,当使用数据库事务时,大部分的场景是在代码中发生了异常时,需要回滚事务,而不发生异常时则是提交事务,从而保证数据库数据的一致性。从这点出发,Spring给了一个约定,如果使用的是声明式事务,那么当你的业务方法不发生异常(或者发生异常,但该异常也被配置信息允许提交事务)时,Spring就会让事务管理器提交事务,而发生异常(并且该异常不被你的配置信息所允许提交事务)时,则让事务管理器回滚事务。

4.2 注解@Transactional配置项

  声明式事务允许自定义事务接口——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{

}

4.3 声明式事务的约定流程

  在声明式事务中,约定十分重要。注解@Transactional可以使用在方法或者类上面,在Spring IoC容器初始化时,Spring会读入这个注解的事务信息,并且保存到一个事务定义类里面(TransactionDefinition接口的子类),以备将来使用。当运行时会让Spring拦截注解标注的某一个方法或者类的所有方法。

  首先Spring通过事务管理器(PlatformTransactionManager的子类)创建事务,榆次同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上设置。而根据传播行为配置采取一种特定的策略,这是Spring根据配置完成的内容,作为你开发者的我们只需要配置,无须编码。

  然后启动开发者提供的业务代码,此时Spring会通过反射的方式调度开发者的业务代码,但是反射的结果可能是正常返回或者产生异常返回,那么它给定的约定是只要发生异常,并且符合事务定义类回滚条件的,Spring就会将数据库事务回滚,否则将数据库事务提交,这也是Spring自己完成的。

  至此,我们可以发现,在Spring的整个事务管理过程中,我们只需要编写业务代码和对事务属性进行配置就可以了,并不需要使用代码对事务的管理进行干预,工作量变少,代码逻辑也更清晰,更利于维护。这样Spring就通过Spring AOP技术使得我们可以把精力放在业务的开发上,而不是控制数据库的资源和事务上。但同时我们也必须清楚,Spring AOP的底层实现原理是动态代理,也就是只有代理对象相互调用才能像AOP那么神奇,在实际开发过程中还是有很多陷阱的。

5、在Spring+MyBatis组合中使用事务

  在前面的小节中,已经在本实验开发测试环境中配置了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,所以每次调用会产生新的事务。

6、注解@Transactional的自调用失效问题

6.1 问题出错演示

  注解@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失效了。

6.2 问题解决办法

  出现这个问题根本原因在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容器,而这个问题也可以通过两个服务类去调用来解决。

7、典型错误用法剖析

7.1 错误使用Service

  互联网普遍采用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会为你创建对应的数据库事务。如果多次调用,则不在同一个事务中,这就会造成不同时提交和回滚不一致问题。

7.2 过长时间占用事务

  在企业的生产系统中,数据库事务资源是最宝贵的资源之一,使用了数据库事务之后,要及时释放数据库事务。换言之,我们应该尽可能地使用数据库事务资源去完成所需工作,但是在一些工作中需要使用到文件、对外连接等操作,而这些操作往往会占用较长时间,针对这些,如果我们作为开发者而不注意这些细节时,就很容易出现系统宕机问题。

  假设在插入角色后还需要操作一个文件,于是我们要改造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方法而言,已经在一个没有事务的环境中运行了,这样当前的请求就不会长期占用数据库事务资源,使得其他并发的请求被迫等待其释放了。其实不仅仅是文件操作,还有一系列系统之间的通信以及一些可能需要花费较长时间的操作,都要注意这个问题,避免长时间占用数据库事务,导致系统性能的低下。

7.3 错误捕获异常

  模拟一段购买商品的代码,其中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和我们的约定流程。

你可能感兴趣的:(Java,数据库)