一篇文章彻底吃透 Spring 的事务实现

开发 前端
编程式的事务管理比较灵活,如果当前操作非常耗时,可以采用编程式的事务管理来提交事务,避免长事务影响数据库性能;其次如果数据操作比较简单时间短,可以采用声明式事务管理,如果使用不当,可能会导致事务失效,因此在实际使用中要多加小心。

一、背景介绍

今天我们接着来聊聊 SpringBoot 事务相关的具体使用方式。

何谓事务?熟悉数据库的同学可能比较熟悉,对于任意一组 SQL 语句,要么全部执行成功,要么全部执行失败;如果中途因为某个原因其中一条语句执行失败,那么先前执行过的语句将全部撤回。

事务控制的好处在于:可以保证数据操作的安全性。比如,当你给某个客户 A 转账 100 块,此时银行需要从你的个人账户中扣除 100 块,然后再给客户 A 账户增加 100 块,这其实是两个动作,假如银行在操作客户 A 的账户时出现故障,此时你个人账户的钱已经被扣除,但对方的账户并没有到账,这将会给客户产生重大损失。有了事务控制之后,当操作对方的账户发生异常时,可以将个人账户中扣除的钱进行撤回,从而保证用户资金账户的安全性。

Java 作为一个高级开发语言,同样支持数据库的事务控制。在上文中,我们了解到所有涉及到数据库的操作,都需要通过数据库连接对象来完成。当我们在操作数据库时,如果想要开启手动事务控制(默认是自动提交),其实通过连接对象的autoCommit参数就可以完成,例如如下示例:

// 1.加载数据库驱动包
Class.forName(DRIVER_CLASS);
// 2.创建一个数据库连接实例
Connection conn = DriverManager.getConnection(JDBC_URL, USER, PASSWORD);
Statement statement = null;
try {
    // 3.设置手动事务提交,默认是true
    conn.setAutoCommit(false);
    // 4.执行多条SQL语句
    statement = conn.createStatement();
    statement.executeUpdate("insert into tb_user(id, name) values(1, 'tom') ");
    statement.executeUpdate("insert into tb_role(id, name) values(1, 'sys') ");
    ...
    // 5.提交事务
    conn.commit();
} catch (SQLException e) {
    // 如果SQL执行异常,回滚事务
    conn.rollback();
}

// 6.关闭连接
statement.close();
conn.close();

了解了 JDBC 的事务控制之后,再来学习 SpringBoot 事务控制就要容易的多,下面我们一起来看看相关的使用方式。

二、SpringBoot 事务

在 Spring 中事务有两种实现方式,分别是编程式事务管理和声明式事务管理。

  • 编程式事务管理:利用的是TransactionTemplate类或者更底层的PlatformTransactionManager事务管理器来控制事务操作,用户可以手动提交或者回滚事务,编程上比较灵活
  • 声明式事务管理:利用的是@Transactional注解对事务进行管理,本质是通过 AOP 对方法前后进行拦截,在目标方法开始之前创建或者加入一个事务,目标方法执行完成之后根据情况进行提交或者回滚事务,使用上比较简单,易上手

当我们使用  SpringBoot 框架来开发项目的时候,SpringBoot 会自动将 Spring 对数据库事务支持的依赖库加载到工程中,无需再次添加相关依赖包。

下面我们以之前介绍的 SpringBoot 整合 mybatis 的工程为例子,利用事务控制来执行多表数据插入操作,一起来看看这两种事务管理的应用方式。

2.1、编程式事务管理

编程式事务管理主要有两种实现方式,第一种是利用TransactionTemplate类来提交事务,编程简单灵活,也是常用的方式之一;另一种是采用PlatformTransactionManager事务管理器来控制事务的提交和回滚。

我们先看看更底层的PlatformTransactionManager接口应用方式。

2.1.1、PlatformTransactionManager 事务管理器

利用PlatformTransactionManager事务管理器来实现事务的操作,示例如下:

@Service
publicclass ApiService {

    privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(ApiService.class);

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void insert(Role role, Menu menu){
        //手动开启事务
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 新增角色信息
            roleMapper.insert(role);
            // 新增菜单信息
            menuMapper.insert(menu);
            // 提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(status);
            LOGGER.error("提交数据异常",e);
        }
    }
}

在执行角色信息插入和菜单信息插入的时候,如果都成功,则提交事务;如果任意其中一个方法失败,整个操作进行回滚。

关于事务管理器,无论采用的是 JPA 还是 JDBC 等,底层的事务管理器都实现自PlatformTransactionManager接口。如果采用的是spring-boot-starter-jdbc或者Mybatis操作数据库,Spring Boot 框架会默认将DataSourceTransactionManager实例作为实现类;如果采用的是spring-boot-starter-data-jpa,框架会默认将JpaTransactionManager实例作为实现类。

关于这一点,我们可以写一个测试方法来查看PlatformTransactionManager接口的实现类,具体如下:

@MapperScan("com.example.mybatis.mapper")
@SpringBootApplication
publicclass Application {

    @Bean
    public Object testBean(PlatformTransactionManager platformTransactionManager){
        System.out.println("transactionManager:" + platformTransactionManager.getClass().getName());
        returnnew Object();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

启动服务,输出结果如下:

transactionManager:org.springframework.jdbc.datasource.DataSourceTransactionManager

2.1.2、TransactionTemplate 事务模板类

除了采用事务管理器来实现事务手动控制,Spring 事务框架还为用户提供了TransactionTemplate事务模板类,通过它也可以实现事务的手动控制,并且操作更加简单,示例如下:

@Service
publicclass ApiService {

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Autowired
    private TransactionTemplate transactionTemplate;

    /**
     * 方式一:带返回值的事务提交
     * @param role
     * @param menu
     */
    public void insert1(Role role, Menu menu){
        Integer result =transactionTemplate.execute(status -> {
            // 新增角色信息
            roleMapper.insert(role);
            // 新增菜单信息
            menuMapper.insert(menu);
            return1;
        });
    }

    /**
     * 方式二:忽略返回值的事务提交
     * @param role
     * @param menu
     */
    public void insert2(Role role, Menu menu){
        transactionTemplate.execute(status -> {
            // 新增角色信息
            roleMapper.insert(role);
            // 新增菜单信息
            menuMapper.insert(menu);
            returnnull;
        });
    }

    /**
     * 方式三:不带返回值的事务提交
     * @param role
     * @param menu
     */
    public void insert3(Role role, Menu menu){
        transactionTemplate.execute(new TransactionCallbackWithoutResult(){

            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                // 新增角色信息
                roleMapper.insert(role);
                // 新增菜单信息
                menuMapper.insert(menu);
            }
        });
    }
}

以上三种方式,都可以实现实现事务的手动控制,效果等同于采用事务管理器来实现事务手动控制。

如果仔细翻查TransactionTemplate类的execute()方法,你会发现它底层的实现逻辑,与上文介绍的利用事务管理器来控制事务的提交和回滚操作类似。

execute()方法的部分核心源码如下!

图片图片

因此,在编程式事务管理方式下,推荐采用TransactionTemplate类来实现,编程上会更加灵活简单。

2.2、声明式事务管理

声明式事务管理就更加简单了,只需要在方法上增加注解@Transactional即可,无需任何配置。

2.2.1、Transactional 注解应用示例

@Service
publicclass ApiService {

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Transactional
    public void insert(Role role, Menu menu){
        // 新增角色信息
        roleMapper.insert(role);
        // 新增菜单信息
        menuMapper.insert(menu);
    }
}

声明式事务管理方式,本质采用的是 AOP 动态代理的方式,对标注@Transactional注解的方法进行前后拦截,然后通过事务管理器来实现事务控制。

尽管@Transactional注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 不推荐在接口或者接口方法上使用该注解,如果编程不当某些场景下可能会失效。当作用于类上,那么该类的所有public方法将都具有事务属性。在实际使用过程中,推荐在类的方法上使用该注解,以便实现精准的事务控制。

2.2.2、Transactional 注解失效场景

在使用@Transactional注解时,有以下几个场景,事务可能不会生效!

  • 场景一:@Transactional注解如果应用在非public方法,事务不会生效,并且不会抛异常,该注解只会代理public修饰的方法
  • 场景二:同一个类中的方法,调用标注@Transactional注解的方法,事务控制也不会生效
  • 场景三:内部异常如果被catch吃了,事务不会回滚
  • 场景四:@Transactional注解默认只对运行时异常或者 Error 才回滚事务,其它场景不会触发事务回滚,如果异常不在范围之内,事务不会回滚
  • 场景五:@Transactional注解上的配置参数使用不当,可能导致事务失效

下面我们每个场景下,错误的用法。

事务失效:场景一

@Transactional注解应该只被应用到public方法上,如果应用在非public方法,事务不会生效,并且不会抛异常,错误示例如下:

@Service
publicclass ApiService {

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Transactional
    protected void insert(Role role, Menu menu){
        // 新增角色信息
        roleMapper.insert(role);
        // 新增菜单信息
        menuMapper.insert(menu);
    }
}

此时,执行insert操作的时候会自动提交,Spring Boot 不会开启事务控制。假如menuService.insert()方法执行异常,此时roleService.insert()提交的数据不会回滚。

原因在于:@Transactional注解只会代理public修饰的方法,由 Spring AOP 代理决定的。

事务失效:场景二

同一个类中的方法,如果调用标注@Transactional注解的方法,事务控制也不会生效,错误示例如下:

@Service
publicclass ApiService {

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;

    public void save(Role role, Menu menu){
        insert(role, menu);
    }

    @Transactional
    public void insert(Role role, Menu menu){
        // 新增角色信息
        roleMapper.insert(role);
        // 新增菜单信息
        menuMapper.insert(menu);
    }
}

当外部调用save()方法来保存数据的时候,此时 Spring Boot 不会开启事务控制,会自动提交数据,如果执行过程中发生异常,之前执行过的数据操作不会回滚。

原因在于:被@Transactional标注的方法,只有被当前类以外的代码调用时,才会由 Spring Aop 生成的代理对象来管理。

事务失效:场景三

被@Transactional标注的方法,内部异常如果被手动catch吃了,事务不会回滚,错误示例如下:

@Service
publicclass ApiService {

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Transactional
    public void insert(Role role, Menu menu){
        try {
            // 新增角色信息
            roleMapper.insert(role);
            // 新增菜单信息
            menuMapper.insert(menu);
        } catch (Exception e){
            e.printStackTrace();
            // todo..
        }
    }
}

此时,被@Transactional标注的方法具备事务控制,如果执行过程中发生异常,数据不会回滚,因为异常被捕获了。当 Spring AOP 事务代理类没有感知到异常时,会自动提交事务。

事务失效:场景四

@Transactional注解默认只对运行时异常或者 Error 才回滚事务,其它场景不会触发事务回滚,如果异常不在范围之内,事务不会回滚,错误示例如下:

@Service
publicclass ApiService {

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Transactional
    public void insert(Role role, Menu menu) throws Exception {
        try {
            // 新增角色信息
            roleMapper.insert(role);
            // 新增菜单信息
            menuMapper.insert(menu);
        } catch (Exception e){
            thrownew Exception("保存错误");
        }
    }
}

此时中途如果插入数据失败,会抛Exception异常,但是之前执行成功的数据不会回滚。

如果想要支持其它类型的异常,可以在@Transactional注解类上配置rollbackFor参数,比如如下示例:

@Transactional(rollbackFor = Exception.class)

这个参数配置仅限于 Throwable 异常类及其子类。

事务失效:场景五

在@Transactional注解类上,其实隐含了很多的事务属性参数,如果参数配置不当,可能也会导致事务失效,错误示例如下:

@Service
publicclass ApiService {

    @Autowired
    private RoleService roleService;

    @Autowired
    private MenuService menuService;

    @Transactional(readOnly = true)
    public void insert(Role role, Menu menu){
        // 新增角色信息
        roleService.insert(role);
        // 新增菜单信息
        menuService.insert(menu);
    }
}

此时提交数据会报错,因为readOnly = true参数表示只读模式,不能对数据库的数据进行更改操作。

三、事务注解详解

在上文中,我们介绍了@Transactional事务注解的基本用法,正如上文所说,在注解类上,其实隐含了很多的事务属性参数,Transactional注解类源码如下。

@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 {};
}

下面我们一起来看看每个属性的作用。

属性

类型

默认值

说明

transactionManager

String

DEFAULT

事务管理器

propagation

Propagation枚举

REQUIRED

事务传播属性

isolation

Isolation枚举

DEFAULT

事务隔离级别

timeout

int

-1

超时(秒)

readOnly

boolean

false

是否只读

rollbackFor

Class[]

{}

需要支持回滚的异常类

rollbackForClassName

String[]

{}

需要支持回滚的异常类名

noRollbackFor

Class[]

{}

不需要支持回滚的异常类

noRollbackForClassName

String[]

{}

不需要支持回滚的异常类名

我们重点看看transactionManager、propagation和isolation这三个参数属性值配置,其它参数基本上见名之意,就不用介绍了。

3.1、事务管理器属性

默认情况下,不需要我们手动配置事务管理器实例。如果 Spring 容器中有多个事务管理器实例,比如多数据源的情况下,某些场景下,就需要我们手动指定事务管理器实例。

具体应用示例如下:

@Configuration
publicclass TransactionManagerConfigBean {

    @Autowired
    private DataSource dataSource;

    /**
     * 自定义一个事务管理器1,同时作为默认事务管理器
     * @return
     */
    @Bean(name = "txManager1")
    @Primary
    public PlatformTransactionManager txManager1() {
        returnnew DataSourceTransactionManager(dataSource);
    }

    /**
     * 自定义一个事务管理器2
     * @return
     */
    @Bean(name = "txManager2")
    public PlatformTransactionManager txManager2() {
        returnnew DataSourceTransactionManager(dataSource);
    }
}

如果需要使用指定的事务管理器,只需要在@Transactional注解中配置相应的参数即可。

@Service
publicclass ApiService {

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Transactional(value = "txManager2")
    public void insert(Role role, Menu menu) throws Exception {
        // 新增角色信息
        roleMapper.insert(role);
        // 新增菜单信息
        menuMapper.insert(menu);
    }
}

3.2、事务传播属性

事务传播属性,指的是当一个方法内同时存在多个事务的时候,Spring 如何处理这些事务的行为。

Spring 支持 7 种事务传播方式,Propagation枚举类支持的属性值如下:

  • REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
  • SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
  • MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
  • REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起
  • NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起
  • NEVER:以非事务方式运行,如果当前存在事务,则抛出异常
  • NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED

如果想要指定事务传播行为,可以通过propagation属性设置,例如:

@Transactional(propagation = Propagation.REQUIRED)

Spring 默认采用的是REQUIRED属性值,也就是说,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

这样设计的好处在于:当一个方法存在多个事务开启的操作时,只会有一个有效的事务实例,可以实现数据的原子性操作。

比如如下示例:

@Service
publicclass ApiService {

    privatestaticfinal Logger LOGGER = LoggerFactory.getLogger(ApiService.class);

    @Autowired
    private RoleService roleService;

    @Autowired
    private MenuService menuService;

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void save(Role role, Menu menu){
        //手动开启事务
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 新增角色信息
            roleService.insert(role);
            // 新增菜单信息
            menuService.insert(menu);
            // 提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(status);
            LOGGER.error("提交数据异常",e);
        }
    }
}
@Service
public class RoleService {

    @Autowired
    private RoleMapper roleMapper;

    @Transactional
    public void insert(Role role){
        roleMapper.insert(role);
    }
}
@Service
publicclass MenuService {

    @Autowired
    private MenuMapper menuMapper;

    @Autowired
    private TransactionTemplate transactionTemplate;

    public void insert(Menu menu){
        transactionTemplate.execute(status -> {
            menuMapper.insert(menu);
            returnnull;
        });
    }
}

当调用ApiService.save()方法时,如果出现异常,所有的操作都会回滚;反之,提交事务。

3.3、事务隔离级别属性

事务隔离级别,可以简单的理解为数据库的事务隔离级别。

从数据库角度,为了解决多个事务操作同一条数据产生的并发问题,提出了事务隔离级别概念,由低到高依次为 Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读等这几类问题,每个隔离级别作用如下:

  • read uncommitted:俗称读未提交,指的是一个事务还没提交时,它做的变更就能被别的事务看到。
  • Read committed:俗称读提交,指的是一个事务提交之后,它做的变更才会被其他事务看到。
  • Repeatable read:俗称可重复读,指的是一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,同时当其他事务在未提交时,变更是不可见的。
  • Serializable:俗称串行化,顾名思义就是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

在 Spring 中,事务隔离级别的设置可以通过Isolation枚举类来指定,其支持的属性值如下:

  • DEFAULT:默认值,表示使用底层数据库的默认隔离级别。大部分数据库,默认隔离级别为可重复读;Mysql 有些例外,采用可重复读隔离级别
  • READ_UNCOMMITTED:对应数据库中读未提交的隔离级别
  • READ_COMMITTED :对应数据库中读提交的隔离级别
  • REPEATABLE_READ :对应数据库中可重复读的隔离级别
  • SERIALIZABLE:对应数据库中串行化的隔离级别

如果想要指定事务隔离级别,可以通过isolation属性设置,例如:

@Transactional(isolation = Isolation.DEFAULT)

四、小结

最后总结一下,编程式的事务管理比较灵活,如果当前操作非常耗时,可以采用编程式的事务管理来提交事务,避免长事务影响数据库性能;其次如果数据操作比较简单时间短,可以采用声明式事务管理,如果使用不当,可能会导致事务失效,因此在实际使用中要多加小心。

本文主要围绕 Spring Boot 事务管理的使用方式,做了一次知识内容的总结,如果有描述不对的地方,欢迎留言指出。

五、参考

1.https://www.cnblogs.com/sharpest/p/7995203.html

2.https://blog.csdn.net/MinggeQingchun/article/details/119579941

责任编辑:武晓燕 来源: 潘志的技术笔记
相关推荐

2024-06-25 08:18:55

2013-04-15 10:59:08

iOS开发ARC版本说明

2022-08-03 08:17:00

Redis事务内存

2019-10-15 10:23:13

服务器MySQL 数据

2024-05-17 10:05:06

Java机制应用

2017-07-20 16:55:56

Android事件响应View源码分析

2022-05-25 08:31:31

ArthasInstrument

2015-07-15 17:09:48

HiveHadoop分布式文件系统

2019-07-23 08:55:46

Base64编码底层

2021-04-07 13:28:21

函数程序员异步

2020-10-09 08:15:11

JsBridge

2024-05-10 08:19:59

arthasjava字节码

2021-06-30 00:20:12

Hangfire.NET平台

2023-05-12 08:19:12

Netty程序框架

2021-04-09 08:40:51

网络保险网络安全网络风险

2022-02-21 09:44:45

Git开源分布式

2019-04-17 15:16:00

Sparkshuffle算法

2017-09-05 08:52:37

Git程序员命令

2022-02-18 00:13:53

JavaScript编程语言数组

2021-11-04 10:34:02

JavaScript继承编程
点赞
收藏

51CTO技术栈公众号