一篇搞懂 Spring事务

  • 什么是事务
  • 事物的特性
  • Spring 事务
  • Spring使用事务
  • Spring事务失效场景
  • Spring事务隔离级别
  • Spring事务传播属性


什么是事务

数据库事务: 数据库事务通常指对数据库进行读或写的一个操作序列;

它的存在包含有以下两个目的:

  1. 为数据库操作提供了一个从失败中恢复到正常状态的方法, 同时提供了数据库即使在异常状态下仍能保持一致性的方法;
  2. 当多个应用程序在并发访问数据库时, 可以在这些应用程序之间提供一个隔离方法, 以防止彼此的操作互相干扰;

系统中的事务: 处理一系列业务处理的执行逻辑单元,该单元里的一系列类操作要不全部成功要不全部失败

为什么使用事务

可以保证数据的一致性和完整性(避免异常和错误等导致的数据信息异常)

事物的特性

  • 原子性(atomicity); 一个事务是一个不可分割的工作单位, 事务中包括的操作要么都做, 要都回滚;
    举例来说, 你去菜市场买鸡蛋,你最少买一个鸡蛋,你不能买半个鸡蛋,
  • 一致性(consistency); 事务必须是使数据库从一个一致性状态变到另一个一致性状态;
    举例来说, 假设你微信钱包有500我的钱包也有500,不管咱俩如何转账,事物结束后咱们俩钱包里的钱加起来还得是1000;
  • 隔离性(isolation); 一个事务的执行不能被其他事务干扰; 即一个事务内部的操作及使用的数据对并发的其他事务是隔离的, 并发执行的各个事务之间不能互相干扰;
  • 持久性(durability); 持久性也称永久性(permanence), 指一个事务一旦提交, 它对数据库中数据的改变应该是永久性的; 接下来的其他操作或故障不应该对其有任何影响;

Spring 事务

Spring事务本质是对数据库事务的支持, 如果数据库不支持事务(例如MySQL的MyISAM引擎不支持事务), 则Spring事务也不会生效;

Spring事务实际使用AOP拦截注解方法, 然后使用动态代理处理事务方法, 捕获处理过程中的异常, Spring事务其实是把异常交给Spring处理;

Spring使用事务

Spring中使用 @Transactional 注解来开启事务;

@Transactional用在方法上对该方法有事务(推荐);
@Transactional用在类上对类中所有方法都有事务(推荐);
@Transactional用在接口上对该接口的所有实现都有事务(不推荐, 但根据业务可以放在接口方法上);

@Transactional参数介绍:

spring里的事务是整体加锁吗_mysql

  1. propagation: 指定事务传播机制, 即当前事务被其他事务调用时, 如何使用事务, 默认值为REQUIRED
  2. isolation: 指定事务隔离级别, 最常用的取值是READ_COMMITTED
  3. noRollbackFor: 指定不回滚的异常, 通常默认值(Exception.class)即可;
  4. readOnly: 指定事务是否为只读, 表示该事物只读取数据, 不进行修改, 可帮助数据库引擎优化事务, 此时需要设置readOnly = true
  5. timeout: 可强制指定回滚的超时时间, 默认单位为秒(s), 默认为本地系统的超时时间;

案例假设:

①保存用户到数据库 ②记录用户操作日志 是一个原子操作;

如果①和②之间出了问题, 如果没有事务的话, 可能导致用户记录到了数据库, 但日志里面却没有记录, 造成业务不完整; 如果加入了事务, 那么就可以避免这种问题;

代码验证:

不加事务的代码:

其中先保存user到数据库, 然后打印1/0, 这步会报错, 然后保存log;

spring里的事务是整体加锁吗_java_02

启动项目, 然后调用接口, 可以看到报了错:/ by zero;

然后我们查看数据库, 发现user表已经有了数据, 也就是报错之前的操作保存到了数据库;

而log表里却没有日志, 即报错之后的数据没有保存成功;

此时就造成了数据的不完整, 两步操作要么应该都完成, 要么应该都失败;

spring里的事务是整体加锁吗_数据_03

我们下面加入事务来解决这个问题

重启项目后继续测试, 发现两个表里都没有数据, 说明事务生效了, 两步操作同时失败;

spring里的事务是整体加锁吗_数据库_04

在Springboot中使用事务非常的方便, 不过如果只有一步操作的话就不需要加入事务, 因为事务也是要耗费更多的资源的;

Spring事务失效场景

  1. 数据库引擎是否支持事务(Mysql 的 MyIsam引擎不支持事务);
  2. 注解所在的类是否被加载为 Bean(是否被Spring 管理);
  3. 注解所在的方法是否为 public 修饰的;
  4. 是否存在自身调用的问题;
  5. 所用数据源是否加载了事务管理器;
  6. @Transactional的扩展配置propagation是否正确;
  7. 异常没有被抛出, 或异常类型错误;
  8. 方法用final修饰或static修饰;
  9. 多线程调用;

数据库引擎不支持事务

这里以 MySQL 为例, 其 MyISAM 引擎是不支持事务操作的, InnoDB 才是支持事务的引擎, 一般要支持事务都会使用 InnoDB;

测试: 我们修改user表的引擎为MyISAM

spring里的事务是整体加锁吗_spring里的事务是整体加锁吗_05

继续测试, 然后我们查看数据库, 发现user表已经有了数据, 也就是报错之前的操作保存到了数据库;

如果要保证事务, 数据库的引擎必须要支持事务才可以;

Bean没有被 Spring 管理

如下面例子所示:

//@Service
public class TransactionTestImpl implements TransactionTest {
    @Transactional
    @Override
    public void addUser() {
     
    }
}

如果此时把 @Service 注解注释掉, 这个类就不会被加载成一个 Bean, 那这个类就不会被 Spring 管理了, 事务自然就失效了;

**@Transaction 可以用在类上、接口上、public方法上, 如果将@Trasaction用在了非public方法上, 事务将无效; **

以下来自 Spring 官方文档:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

在使用代理时, 应该只对具有公共可见性的方法应用@Transactional注释; 如果使用@Transactional注释注释了受保护的、私有的或包可见的方法, 则不会引发错误, 但注释的方法不会显示配置的事务设置; 如果需要对非公共方法进行注释, 请考虑使用Aspectj;

自身调用问题

本类方法不经过代理, 无法进行增强, 必须通过代理对象访问方法, 事务才会生效;

@Override
    public void addUser() {
        add();
    }
    
    @Transactional
    public void add() {
        User user = new User(1, "张三", 18);
        // 保存用户
        userMapper.insert(user);

        // 1/0会出现异常
        System.out.println(1 / 0);

        Log log = new Log(user.getId(), "create user :" + user.getId());
        // 保存日志
        logMapper.insert(log);
    }

异常没有抛出

Spring事务只有捕捉到了业务抛出去的异常, 才能进行后续的处理, 如果业务自己捕获了异常, 则事务无法感知;

@Transactional
    @Override
    public void addUser() {
        add();
    }

    // @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void add() {
        User user = new User(1, "张三", 18);
        // 保存用户
        userMapper.insert(user);

        try {
            // 1/0会出现异常
            System.out.println(1 / 0);
        } catch (Exception e) {

        }
        Log log = new Log(user.getId(), "create user :" + user.getId());
        // 保存日志
        logMapper.insert(log);
    }

异常类型错误

并不是任何异常情况下, Spring都会回滚事务, 默认情况下, RuntimeException和Error的情况下, Spring事务才会回滚;

@Transactional
    @Override
    public void addUser() throws Exception {
        add();
    }

    public void add() throws Exception {
        User user = new User(1, "张三", 18);
        // 保存用户
        userMapper.insert(user);

        // 1/0会出现异常
        try {
            System.out.println(1 / 0);
        } catch (Exception e) {
            throw new Exception("更新错误");
        }
        Log log = new Log(user.getId(), "create user :" + user.getId());
        // 保存日志
        logMapper.insert(log);
    }
}

方法用final修饰或static修饰

因为Spring事务是用动态代理实现, 因此如果方法使用了final修饰, 则代理类无法对目标方法进行重写, 植入事务功能;

@Transactional
    public final boolean add(User user, UserService userService) {
        boolean isSuccess = userService.save(user);
        try {
            int i = 1 % 0;
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return isSuccess;
    }

多线程调用

因为Spring的事务是通过数据库连接来实现, 而数据库连接Spring是放在threadLocal里面; 同一个事务, 只能用同一个数据库连接; 而多线程场景下, 拿到的数据库连接是不一样的, 即是属于不同事务;

@Transactional
    @Override
    public void addUser() {
        add();
    }

    // @Transactional()
    public void add() {
        Runnable runnable = () -> {
            User user = new User(1, "张三", 18);
            // 保存用户
            userMapper.insert(user);

            // 1/0会出现异常
            System.out.println(1 / 0);

            Log log = new Log(user.getId(), "create user :" + user.getId());
            // 保存日志
            logMapper.insert(log);
        };
        new Thread(runnable).start();
    }

事务失效总结:

Spring 事务失效的场景有很多, 但是可以分为这几类

是否支持事务: 数据库是否支持事务, 多线程调用也不支持事务;

异常的类型和捕获: Spring必须感知到异常才能开启事务, 默认情况下, RuntimeException和Error异常才会回滚;

方法修饰符: Spring事务只对public方法生效, final和static修饰的方法也不会生效;

内部调用: 一个类中的A方法(没事物)调用B方法(有事物), AB方法事务都不会生效;

方法能否生成代理: 方法要被Spring容器管理事务才能生效, 方法如果不能被重写也不会生成代理;

不同Service方法间调用: 当@Transactional 注解作用在方法A上时, 事务起作用, 方法A中的数据回滚, 方法saveClassInfo中的数据回滚;

当@Transactional 注解作用在方法saveClassInfo上时, 事务对A不起作用, 方法A中的数据提交, 方法saveClassInfo数据回滚;

Spring事务隔离级别

Spring事务本质上使用数据库事务, 而数据库事务本质上使用数据库锁, 所以Spring事务本质上使用数据库锁, 开启Spring事务意味着使用数据库锁;

Spring事务隔离级别比数据库事务隔离级别多一个default;

  1. Default(默认)
    这是一个PlatfromTransactionManager默认的隔离级别, 使用数据库默认的事务隔离级别; 另外四个与JDBC的隔离级别相对应;
  2. ReadUncommitted(读未提交)
    这是事务最低的隔离级别, 它允许另外一个事务可以看到这个事务未提交的数据; 这种隔离级别会产生脏读, 不可重复读和幻像读;
  3. ReadCommitted(读已提交)
    保证一个事务修改的数据提交后才能被另外一个事务读取, 另外一个事务不能读取该事务未提交的数据; 这种事务隔离级别可以避免脏读出现, 但是可能会出现不可重复读和幻读;
  4. RepeatableRead(可重复读)
    这种事务隔离级别可以防止脏读、不可重复读, 但是可能出现幻读; 它除了保证一个事务不能读取另一个事务未提交的数据外, 还保证了不可重复读;
  5. Serializable(串行化)
    这是花费最高代价但是最可靠的事务隔离级别, 事务被处理为顺序执行; 除了防止脏读、不可重复读外, 还避免了幻读;

隔离级别越高, 越能保证数据的完整性和一致性, 但是对并发性能的影响也越大;
对于多数应用程序, 可以优先考虑把数据库系统的隔离级别设为Read Committed; 它能够避免脏读取, 而且具有较好的并发性能;

Spring事务传播属性

Spring在TransactionDefinition接口中规定了7种事务传播行为, 他们规定了事务和事务之间发生嵌套时事务如何进行传播;

所谓事务的传播行为是指, 如果在开始当前事务之前, 一个事务上下文已经存在, 此时有若干选项可以指定一个事务性方法的执行行为;

  1. Required
    默认事务类型, 如果没有, 就新建一个事务;如果有, 就加入当前事务; 适合绝大多数情况;
  2. RequiresNew
    如果没有, 就新建一个事务;如果有, 就将当前事务挂起;
  3. Nested
    如果当前存在事务, 则在嵌套事务内执行; 如果当前没有事务, 则执行与PROPAGATION_REQUIRED类似的操作; 特点就是会设置回滚点;
  4. Supports
    如果没有, 就以非事务方式执行;如果有, 就使用当前事务;
  5. NotSupported
    如果没有, 就以非事务方式执行;如果有, 就将当前事务挂起; 即无论如何不支持事务;
  6. Never
    如果没有, 就以非事务方式执行;如果有, 就抛出异常;
  7. Mandatory
    如果没有, 就抛出异常;如果有, 就使用当前事务; 即强制要有事务;