Spring 事务管理简介

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 事务简介所谓事务,指的是程序中可运行的不可分割的最小单位。在生活中事务也是随处可见的。比方说你在Steam上剁手买了一款游戏,那么付款就是一个事务,要么付款成功,游戏到手;要么付款失败,钱退回你账户。

事务简介

所谓事务,指的是程序中可运行的不可分割的最小单位。在生活中事务也是随处可见的。比方说你在Steam上剁手买了一款游戏,那么付款就是一个事务,要么付款成功,游戏到手;要么付款失败,钱退回你账户。不可能也绝不应该出现花了钱游戏却没到的情况。所以,事务也应该具有两个操作:成功时候提交,或者失败时候回滚

许多框架也提供事务管理功能。JDBC中,你可以关闭自动提交,然后使用Connection.commit()Connection.rollback()执行提交或回滚。在Hibernate中,也有类似的功能。但是,这些框架的事务管理有一个问题,就是它们虽然提供了事务功能,但是为了使用这些功能,你必须在每个需要事务的地方添加额外代码,当执行正常时提交,出现异常时回滚。这样一来,程序中就会出现大量重复的事务管理代码(有过这种经历的人应该能够感同身受吧)。

另外事务还分为本地事务和全局事务。JDBC事务、Hibernate事务都是本地事务,只关注特定资源的事务管理。全局事务则用来控制多个数据库、消息队列等等。Spring提供了统一的事务管理来操作全局事务和本地事务,让我们的代码更加简洁。

Spring事务管理

Spring事务的核心接口是org.springframework.transaction.PlatformTransactionManager

public interface PlatformTransactionManager {

    TransactionStatus getTransaction(
            TransactionDefinition definition) throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}

提交和回滚方法我们已经了解了。getTransaction方法会根据给定的事务定义,返回一个事务状态对象。事务定义包含了事务的一些特征:是否是只读的,超时设置、事务的隔离和传播等。Spring实现了几个PlatformTransactionManager,用于不同环境(JDBC、Hibernate、JPA等等)下的事务管理。我们在使用的时候只要设置好相应的PlatformTransactionManager即可。事务管理包括在Spring核心包中,所以只要项目中添加了spring-core.jar,那么就可以使用Spring的事务管理功能了。如果需要和Hibernate等框架的集成,那么还需要spring-orm.jar

声明式事务管理

Spring支持声明式和编程式两种方式来控制事务管理。最流行的方式就是使用声明式。利用声明式事务管理,我们可以设置遇到什么异常时候回滚事务、在哪些方法上执行事务,而不用修改任何代码。如果已经了解了Spring AOP的话,应该可以猜得到,Spring声明式事务管理需要AOP代理的支持。下面我们来用一个例子说明一下。

配置PlatformTransactionManager

下面我们使用JDBC来演示一下Spring事务管理。因此首先需要配置相应的PlatformTransactionManager,在这里是DataSourceTransactionManager,它需要相应的数据源来初始化。

<!--定义事务管理器-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--定义数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
      destroy-method="close">
    <property name="url" value="jdbc:mysql://localhost:3306/test"/>
    <property name="username" value="root"/>
    <property name="password" value="12345678"/>
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <constructor-arg ref="dataSource"/>
</bean>

然后我们需要一个类来进行事务管理。首先我们定义一个接口。

public interface UserService {
    void add(User user);

    User get(String username);
}

然后来实现这个接口。在这里我用了Spring JDBC来进行数据操作。throwException来模拟抛出异常的情况。

public class JdbcUserService implements UserService {

    private JdbcTemplate template;
    private boolean throwException;

    @Autowired
    public void setTemplate(JdbcTemplate template) {
        this.template = template;
    }

    public void setThrowException(boolean throwException) {
        this.throwException = throwException;
    }


    @Override
    public void add(User user) {
        template.update("INSERT INTO user(name) VALUES(?)", user.getName());
        if (throwException) {
            throw new RuntimeException("抛出异常");
        }
    }

    @Override
    public User get(String username) {
        return template.queryForObject("SELECT id,name FROM user WHERE name=?", new UserRowMapper(), username);
    }
}

配置事务管理

然后我们需要配置一个切入点和通知,指定哪些方法应用什么样的事务管理,这一部分属于AOP的部分,这里不再细述。我们需要<tx:advice>节点设置事务管理,该节点需要设置标识符和事务管理器。<tx:attributes>节点中的配置表示,以get开头的方法是只读的,其他方法不是只读的。这有助于Spring正确设置事务。

<tx:advice id="txAdvice"
           transaction-manager="dataSourceTransactionManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true"/>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>
<!--使用AOP设置事务管理-->
<aop:config>
    <aop:pointcut id="userService"
                  expression="execution(* yitian..transaction.JdbcUserService.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="userService"/>
</aop:config>

回滚操作

默认情况下,Spring会在方法中抛出运行时错误时执行回滚,如果方法中抛出受检异常则不回滚。我们可以向<tx:method/>节点添加rollback-for属性来指定当方法抛出什么异常时执行回滚,这里的异常可以是某一个具体的异常,也可以是一些异常的父类。

<tx:attributes>
    <tx:method name="get*" read-only="true"/>
    <tx:method name="*" rollback-for="Exception"/>
</tx:attributes>

相应的,还有一个no-rollback-for属性来配置在遇到某些异常下不执行回滚操作。

<tx:attributes>
    <tx:method name="get*" read-only="true"/>
    <tx:method name="*" no-rollback-for="IndexOutOfBoundsException"/>
</tx:attributes>

如果遇到了多个回滚规则,以最具体的那一个为准。所以下面的配置,当遇到InstrumentNotFoundException时不会回滚,当遇到其他异常时则执行回滚。

<tx:advice id="txAdvice">
    <tx:attributes>
    <tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
    </tx:attributes>
</tx:advice>

事务传播级别

上面简单使用了<tx:advice/>节点。下面我们来分析一下这个节点的配置。默认情况下,<tx:advice/>的配置如下:

  • 传播设置为:REQUIRED
  • 隔离级别是:DEFAULT
  • 事务是可读可写的
  • 事务超时是底层事务系统的默认超时,如果底层不支持就没有超时
  • 任何运行时异常会触发回滚,任何受检异常不触发回滚。

首先来说明一下事务的传播,这指的是外层事务和内层事务之间的关系。常用的值有三个:PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEWPROPAGATION_NESTED

首先来说说PROPAGATION_REQUIRED。当外层事务需要一个内层事务的时候,会直接使用当前的外层事务。这样一来多个方法可能会共享同一个事务。如果内层事务出现回滚,那么外层事务会也会回滚。这种情况下内层事务会抛出一个UnexpectedRollbackException异常,外层调用者需要捕获该异常来判断外层事务是否已回滚。

PROPAGATION_REQUIRES_NEW会为每个事务创建完全独立的事务作用域,如果外层事务需要一个内层事务,内层事务会先挂起外层事务,当内层事务执行完毕之后会恢复外层事务。这样一来内外层事务的提交和回滚完全是独立的,不会互相干扰。

PROPAGATION_NESTED使用带有多个保存点的单个事务。这些保存点会映射到JDBC的保存点上。所以只能用于JDBC环境和DataSourceTransactionManager中。

事务的隔离级别,除了DEFAULT之外,剩下的几种隔离级别和JDBC中的隔离级别一一对应。详细情况请查看JDBC的相关文档。

测试事务

前面都配置完成之后,我们就可以测试一下Spring的事务管理功能。下面使用了一个测试类来测试。设置userService.setThrowException(false)之后,事务不会抛出异常,我们可以看到成功插入了用户数据。当设置userService.setThrowException(true),事务会抛出异常,我们发现这次没有插入数据。

@ContextConfiguration(locations = {"classpath:transaction.xml"})
@RunWith(SpringRunner.class)
public class TransactionTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private JdbcUserService userService;

    @Before
    public void before() {
        jdbcTemplate.execute("CREATE TABLE user(id INT AUTO_INCREMENT PRIMARY KEY ,name VARCHAR(255))");
    }

    @After
    public void after() {
        jdbcTemplate.execute("DROP TABLE IF EXISTS user");
    }

    @Test
    public void testTransaction() {
        //事务成功
        userService.setThrowException(false);
        User user = new User();
        user.setName("yitian");
        userService.add(user);
        User u = userService.get(user.getName());
        assertEquals(user.getName(), u.getName());
        //事务失败
        userService.setThrowException(true);
        user.setName("liu6");
        u = null;
        try {
            userService.add(user);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        try {
            //由于没有记录,Spring JDBC会抛出异常
            //所以必须捕获该异常
            u = userService.get(user.getName());
        } catch (EmptyResultDataAccessException e) {
            System.out.println(e.getMessage());
        }

        assertNull(u);

    }
}

注解配置

前面用的都是XML配置方式,还可以使用注解配置声明式事务管理。这需要在配置文件中添加一行,这一行仍然需要指明使用的事务管理器。

<tx:annotation-driven transaction-manager="txManager"/>

如果你全部使用注解配置,那么在标记了@Configuration的类上在添加@EnableTransactionManagement即可。需要注意这两种方法都会在当前ApplicationContext中寻找@Transactional Bean。

注解配置主要使用@Transactional注解,该注解可以放置到类、接口或者公有方法上。该注解还有一些属性和XML配置相对应。但是根据配置的不同,注解可能不会起作用。下面是Spring官方的两段话。

Spring建议你只在具体类上应用注解@Transactional注解,而不是注解到接口上。你可以将注解应用到接口(或者接口方法)上,但是这只在你知道你在用基于接口的代理时起作用。实际上,Java注解不会从接口继承,这意味着如果你使用基于类的代理(proxy-target-class="true")或者基于编织的切面( mode="aspectj"),那么事务设置不会被代理和编织体系识别,事务对象也不会被包装到事务代理中,这毫无疑问是件坏事。

在代理模式(这是默认选项)中,只有显式经过代理的方法调用会被拦截。这意味着自我调用,也就是目标对象中的一个方法调用该目标对象的另一个方法,不会在运行时触发事务,即使该方法标记了@Transactional。同样的,代理必须完全初始化来提供期望的行为,所以你不应该在初始化代码中依赖这样的功能(例如@PostConstruct)。

如果需要使用多个事务管理器,可以像下面这样。

public class TransactionalService {

   @Transactional("order")
   public void setSomething(String name) { ... }

   @Transactional("account")
   public void doSomething() { ... }
}

配置文件中,事务管理器需要使用<qualifier>节点指定不同的名称。

<tx:annotation-driven/>

    <bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
        <qualifier value="order"/>
    </bean>

    <bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
        <qualifier value="account"/>
    </bean>

如果要应用的注解有很多地方重复,可以将它们定义为一个自定义注解,然后使用自定义注解应用到需要的地方。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}

Hibernate事务管理

前面简单使用了JDBC的事务管理。不过实际上大部分人应该都需要使用Hibernate等高级框架的事务管理功能。下面也做一些介绍。

首先需要一个接口,定义我们需要的操作。

public interface UserDao {
    void add(User user);

    User get(String name);
}

然后我们实现这个接口,定义hibernate数据访问。这里使用了HibernateTemplate类,这个类是Spring提供的,我们可以使用这个类简化Hibernate操作。我们可以看到使用这个类不需要操作Session,Session会由Spring自动管理。当然,这里为了使用Hibernate的自然主键,所以还是需要直接使用Session来查找自然主键。

public class DefaultUserDao implements UserDao {
    private HibernateTemplate template;
    private SessionFactory sessionFactory;

    @Autowired
    public void setTemplate(HibernateTemplate template) {
        this.template = template;
    }

    @Autowired
    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    public void add(User user) {
        template.save(user);
    }

    @Override
    public User get(String name) {
        Session session = sessionFactory.getCurrentSession();
        return session.bySimpleNaturalId(User.class).load(name);
    }
}

然后是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:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd   http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--设置数据源-->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"/>
        <property name="username" value="root"/>
        <property name="password" value="12345678"/>
    </bean>
    <!--设置SessionFactory-->
    <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="annotatedClasses">
            <list>
                <value>yitian.learn.hibernate.User</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">create</prop>
            </props>
        </property>
    </bean>
    <!--设置hibernate模板-->
    <bean id="hibernateTemplate" class="org.springframework.orm.hibernate5.HibernateTemplate">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
    <!--设置hibernate事务管理器-->
    <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
    <!--用户数据访问对象-->
    <bean id="userDao" class="yitian.learn.hibernate.DefaultUserDao"/>
    <!--设置事务管理-->
    <tx:advice id="txAdvice"
               transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
    <!--使用AOP设置事务管理-->
    <aop:config>

        <aop:pointcut id="userDaoPointcut"
                      expression="execution(* yitian.learn.hibernate.DefaultUserDao.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="userDaoPointcut"/>

    </aop:config>
</beans>

最后使用一个测试类来测试一下代码。可以看到,在操作数据上我们完全没有使用Hibernate的事务API,完全交由Spring管理。当然如果抛出异常,Spring也会回滚。

@ContextConfiguration(locations = {"classpath:hibernate-bean.xml"})
@RunWith(SpringRunner.class)
public class HibernateTransactionTest {
    @Autowired
    private UserDao userDao;

    @Test
    public void testHibernateTransaction() {
        User user = new User();
        user.setUsername("yitian");
        user.setPassword("1234");
        user.setNickname("易天");
        user.setBirthday(LocalDate.now());
        userDao.add(user);

        User u = userDao.get(user.getUsername());
        System.out.println(u);
        assertEquals(user.getNickname(), u.getNickname());
    }
}

代码在Csdn code,有兴趣的同学可以看看。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
3天前
|
Java 开发者 Spring
Spring高手之路24——事务类型及传播行为实战指南
本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
11 1
Spring高手之路24——事务类型及传播行为实战指南
|
4月前
|
Java 关系型数据库 MySQL
Spring 事务失效场景总结
Spring 事务失效场景总结
61 4
|
2月前
|
Java 数据库连接 数据库
spring复习05,spring整合mybatis,声明式事务
这篇文章详细介绍了如何在Spring框架中整合MyBatis以及如何配置声明式事务。主要内容包括:在Maven项目中添加依赖、创建实体类和Mapper接口、配置MyBatis核心配置文件和映射文件、配置数据源、创建sqlSessionFactory和sqlSessionTemplate、实现Mapper接口、配置声明式事务以及测试使用。此外,还解释了声明式事务的传播行为、隔离级别、只读提示和事务超时期间等概念。
spring复习05,spring整合mybatis,声明式事务
|
2月前
|
Java 测试技术 数据库
Spring事务传播机制(最全示例)
在使用Spring框架进行开发时,`service`层的方法通常带有事务。本文详细探讨了Spring事务在多个方法间的传播机制,主要包括7种传播类型:`REQUIRED`、`SUPPORTS`、`MANDATORY`、`REQUIRES_NEW`、`NOT_SUPPORTED`、`NEVER` 和 `NESTED`。通过示例代码和数据库插入测试,逐一展示了每种类型的运作方式。例如,`REQUIRED`表示如果当前存在事务则加入该事务,否则创建新事务;`SUPPORTS`表示如果当前存在事务则加入,否则以非事务方式执行;`MANDATORY`表示必须在现有事务中运行,否则抛出异常;
119 4
Spring事务传播机制(最全示例)
|
6天前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
23 0
|
1月前
|
Java 关系型数据库 MySQL
Spring事务失效,我总结了这7个主要原因
本文详细探讨了Spring事务在日常开发中常见的七个失效原因,包括数据库不支持事务、类不受Spring管理、事务方法非public、异常被捕获、`rollbackFor`属性配置错误、方法内部调用事务方法及事务传播属性使用不当。通过具体示例和源码分析,帮助开发者更好地理解和应用Spring事务机制,避免线上事故。适合所有使用Spring进行业务开发的工程师参考。
27 2
|
1月前
|
Java 程序员 Spring
Spring事务的1道面试题
每次聊起Spring事务,好像很熟悉,又好像很陌生。本篇通过一道面试题和一些实践,来拆解几个Spring事务的常见坑点。
Spring事务的1道面试题
|
2月前
|
Java Spring
Spring 事务传播机制是什么?
Spring 事务传播机制是什么?
22 4
|
2月前
|
XML 监控 Java
Spring Cloud全解析:熔断之Hystrix简介
Hystrix 是由 Netflix 开源的延迟和容错库,用于提高分布式系统的弹性。它通过断路器模式、资源隔离、服务降级及限流等机制防止服务雪崩。Hystrix 基于命令模式,通过 `HystrixCommand` 封装对外部依赖的调用逻辑。断路器能在依赖服务故障时快速返回备选响应,避免长时间等待。此外,Hystrix 还提供了监控功能,能够实时监控运行指标和配置变化。依赖管理方面,可通过 `@EnableHystrix` 启用 Hystrix 支持,并配置全局或局部的降级策略。结合 Feign 可实现客户端的服务降级。
153 23