Spring 事务管理


一、事务管理

1. 场景应用

事务在一个应用中的作用是举足轻重轻重的,合理利用事务能够对保证数据的一致性与完整性。

举个常见的例子, user-1user-2 转账了 1000 元,在理想的情况下,在 user-1 发出转账后账户余额将减少,而 user-2 在收到转账之后余额对应进行增加,流程如下图所示:

但在异常的场景中,当 user-1 扣款成功之后由于网络异常等问题导致 user-2 并没有收到实际的收款,但此时 user-1 已经完成了扣款,导致了系统的总金额减少 1000 元。

此类的数据不一致性对于系统设计而言是相当致命的,而事务的也应运而生。在事务的场景下,上述的流程在任意步骤发生异常时,都将执行数据回滚,只有在整个流程成功结束后,数据才会进行提交进行更新。

2. 基本使用

Spring 工程中使用注解相对较为简单,在启动类上添加 @EnableTransactionManagement 开始事务管理并通过 @Transactiona 注解即可实现对事件开启事务。

@SpringBootApplication
// 开启事务管理
@EnableTransactionManagement
public class TransactionApplication {

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

}

@Transactiona 注解可作用于类上或方法上,当注解作用于类上时将会为类中所有声明为 pulbic 的方法开启事务,当作用于单个方法时则只为当前方法开启事务,为了细化事务粒度,更推荐使用后者配置事务。

如下述示例中则会同时为 demo1()demo2() 两个方法开启事务。

@Transactional
public class TestService {
    
    public void demo1() {
       // do somethiong
    }

    public void demo2() {
       // do somethiong
    }
}

倘若在类与方法上同时声明了 @Transactiona 注解,那么方法上的注解配置将覆盖类上的配置。即方法上的注解配置优先级高于类上的注解配置,类上事务注解优点在于其作用范围更广。同时需要注意的一点时 @Transactiona 作用于方法时方法必须声明为 public,否则将不会生效。

二、注解参数

@Transactiona 注解中提供一系列参数用于配置注解的作用效果,下面将分别进行介绍。

1. propagation

通过 propagation 可指定事务的传播行为,其描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。

Spring 中事务的传播行为 Propagation 可取参数如下:

参数 描述
REQUIRED 默认值,如果有事务则加入事务,没有的则新建事务。
NOT_SUPPORTED 容器不为这个方法开启事务。
REQUIRES_NEW 不管是否存在都新建一个事务,将原事务挂起,待新的执行完毕继续执行老的事务。
MANDATORY 必须在一个已有的事务中执行,否则抛出异常。
NEVER 必须在一个没有的事务中执行,否则抛出异常,与 MANDATORY 相反。
SUPPORTS 如果其他调用这个方法的方法声明了事务则使用其事务,若无则就不使用事务。

在没有显式指定 propagation 的前提下即采用默认值 REQUIRED,将会为事务方法新建一个事务并执行方法体内容,在发生异常时事务方法体中执行的数据操作将会执行回滚操作。

下述示例中,saveRecord() 方法分别调用了两次新增方法,其中第二次为事务方法,并在事务方法中模拟抛出异常测试回滚。运行程序后调用接口,在执行到第二次插入后由于 @Transactional 的存在将为该方法开启事务,在提示报错之后查看数据库将会看到第一次新增操作成功提交,而第二次操作插入 id = 456 的数据实现了异常回滚库中并没有该记录。

@RestController
@RequestMapping("/api/propagation")
public class PropagationController {

    @Autowired
    private SysUserDao sysUserDao;

    @Autowired
    private PropagationService propagationService;

    @PostMapping("insert")
    public void saveRecord(@RequestBody SysUser sysUser) {
        // 第一次新增
        sysUserDao.insert(sysUser);

        sysUser.setId("456");
        sysUser.setName("Beth");
        // 第二次新增
        propagationService.insert(sysUser);
    }
}

@Service
public class PropagationServiceImpl implements PropagationService {

    @Autowired
    private SysUserDao sysUserDao;

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public int insert(SysUser user) {
        int i = sysUserDao.insert(user);
        // 模拟异常
        if (Objects.equals(user.getId(), "456")) {
            throw new RuntimeException();
        }
        return i;
    }
}

稍微调整一下上述的示例为下述形式,为方法 insert() 开始事务并在其中执行两次插入操作,同上述步骤调用接口后可以发现两次插入都实现了回滚操作。

可以发现我们无需为每个方法手动添加注解开启事务,如示例中第二次插入 save() 是在事务方法 insert() 范围内调用的,其操作也将同样包含于事务内。

@Service
public class PropagationServiceImpl implements PropagationService {

    @Autowired
    private SysUserDao sysUserDao;
    @Autowired
    private UserService userService;

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public int insert(SysUser user) {
        // 第一次插入
        sysUserDao.insert(user);

        // 第二次插入
        return userService.save(user);
    }
}

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private SysUserDao sysUserDao;

    @Override
    public int save(SysUser user) {
        int i = sysUserDao.insert(user);
        if (Objects.equals(user.getId(), "456")) {
            throw new RuntimeException();
        }
        return i;
    }
}

如果想要两个方法执行互不影响,可以为 save() 方法也添加事务并设置传播行为 propagationREQUIRES_NEW,即为其创建一个新的事务,如此一来两个方法将处于两个不同的事务之中,save() 异常回滚将独立于 insert() 方法,直观的效果来看就是 insert() 第一次执行插入成功而第二次失败回滚。

2. isolation

事务的传播行为是为了解决不同方法间的事务关联关系,而 isolation 则是用于配置不同事务操作同一资源时的隔离程度。

Spring 的事务隔离级别可取参数与 MYSQL 类似,具体的可选值如下:

参数 描述
READ_UNCOMMITTED 读取未提交数据,若事务开始写数据,则其它事务仅允许读该数据,可能出现脏读。
READ_COMMITTED 读取已提交数据,若是写事务将会禁止其他事务访问该数据,避免了脏读但存在不可重复读。
REPEATABLE_READ 读取数据的事务将会禁止写事务(允许读事务),写事务则禁止任何其他事务(包括了读写),但存在幻读问题。
SERIALIZABLE 要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,保证了数据的一致性但性能较低。

3. readOnly

通过 readOnly 表示当前事务是否为只读事务,默认值为 false,若设置为 true 则在方法体内执行写操作将会抛出异常。

如下述示例中设置了 readOnly = true 标明为只读事务,但却执行了插入操作,因此将会抛出异常。

@Transactional(readOnly = true)
public int insert(SysUser user) {
    return sysUserDao.insert(user);
}

4. rollbackFor

通过 rollbackFor 可指定在特定的异常触发回滚,默认为监控任意异常抛出。

如下示例中指定类在抛出 IllegalArgumentException 异常时进行事务回滚。

@Transactional(rollbackFor = IllegalArgumentException.class)
public int insert(SysUser user) {
    int i = sysUserDao.insert(user);

    if (Objects.equals(user.getId(), "456")) {
        // 抛出 IllegalArgumentException 触发回滚
        throw new IllegalArgumentException();
    }
    return i;
}

5. noRollbackFor

noRollbackForrollbackFor 作用相反,即定义在何种情况下不进行回滚。

如下示例中定义了只要出现异常即进行事务回滚,但对于 IllegalArgumentException 异常则不回滚。因此,在执行到 id 判断抛出 IllegalArgumentException 后并不会执行数据回滚操作。

@Transactional(
        rollbackFor = Exception.class,
        noRollbackFor = IllegalArgumentException.class
)
public int insert(SysUser user) {
    int i = sysUserDao.insert(user);
    if (Objects.equals(user.getId(), "456")) {
        // 抛出 IllegalArgumentException 但不会触发回滚
        throw new IllegalArgumentException();
    }
    return i;
}

三、手动事务

1. 基本介绍

通过 @Transactional 注解方式我们可以快速集成事务到业务中,不过在 Spring 中同样提供了手动事务的开启与提交功能。

SpringIOC 容器中针对 bean 的有一系列的 BeanDefinition 用于描述定义 bean 对象的基本信息,例如 GenericBeanDefinition 等等。对于事务而言,同样也存在 TransactionDefinition 用于定义事务信息。

对于一个事务的描述,最核心的当然就是隔离级别以及传播行为,TransactionDefinition 也提供了相对应的设置方法:

方法 描述
setName() 设置事务名称,可重复。
setIsolationLevel() 设置事务隔离级别。
setPropagationBehavior() 设置事务传播行为。

2. 创建方式

了解了 TransactionDefinition 的基本信息之后下面通过示例介绍如何手动创建事务对象。

public TransactionDefinition getTransactionDefinition() {
    DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
    // 事务名称
    definition.setName("transaction");
    // 隔离级别
    definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
    // 传播行为
    definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
    return definition;
}

当创建了事务描述对象之后即可交由 DataSourceTransactionManager 执行事务的管理,其几个常用方法如下:

方法 描述
getTransaction() 开启事务。
commit() 提交事务。
rollback() 回滚事务。

同样的,通过 getTransaction() 开启事务后将会返回一个状态值,通过其对事务进行手动管理。

@Service
public class TransactionConfig {

    @Autowired
    private DataSourceTransactionManager transactionManager;

    public void manualTransaction() {
        // 创建事务定义
        DefaultTransactionDefinition definition = getTransactionDefinition();
        // 开启事务
        TransactionStatus status = transactionManager.getTransaction(definition);
        try {
            // 执行数据库操作
            // dbOperation();

            // 提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 回滚事务
            transactionManager.rollback(status);
        }
    }

    public DefaultTransactionDefinition getTransactionDefinition() {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        // 事务名称
        definition.setName("transaction");
        // 隔离级别
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        // 传播行为
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        return definition;
    }
}

四、事务失效

1. 自我引用

注意在 Spring 中无法实现在事务定义类中调用事务方法,否则将会导致代理失败从而引起事务不生效。

即事务的定义与调用不应该处于同一个类中,如下述示例中 demo()action() 方法处于同一个类中,在 demo() 中调用 action() 将导致事务失效。

@Service
public class TestService {

    public void demo() {
        // 在同个类中调用事务方法将失效
        action();
    }
    
    @Transactional
    public void action() {
        // dosomething

    }
}

对于此类的自我调用的有两种处理方式,最简单的方式当然就是将事务方式定义到其它类中,但对应同一模块的业务而言则会导致代码分散。

针对此类场景,在 Spring 中提供了 AopContext 获取代理对象从而避免事务的自我调用导致的事务失效,使用方式也相对简单,改造上述的示例为下述即可:

@Service
public class TestServiceA {

    public void demo() {
        // 创建代理对象
        TestService proxy = (TestService) AopContext.currentProxy();
        // 通过代理对象调用事务方法
        proxy.action();
    }
    
    @Transactional
    public void action() {
        // dosomething

    }
}

需要在使用上述的 AopContext 时需要在工程启动类上添加 @EnableAspectJAutoProxy 注解用于暴露代理对象。

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class TestApplication {

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

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录