Bootstrap

【09】Spring笔记--声明式事务

【09】Spring笔记–声明式事务

一、声明式事务的使用

对于声明式事务,使用@Transactional 注解 进行标注即可,可以放在类或方法上,Spring就会产生AOP的功能,这是Spring事务的底层实现

  • 放在类上,该类的所有公共非静态方法都将启用事务功能
  • 放在方法上,就代表这个方法启用事务

@Transactional 中,可配置许多属性,比如事务的隔离级别和传播行为,或者回滚策略

当启动事务时,就会根据事务定义器内的配置去设置事务,首先根据传播行为去确定事务的策略,然后是隔离级别、超时时间、只读等内容设置

查看 @Transactional 源码

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    // 通过Bean name指定事务管理器
    @AliasFor("transactionManager")
    String value() default "";

    // 同value属性
    @AliasFor("value")
    String transactionManager() default "";

    // 传播行为设置
    Propagation propagation() default Propagation.REQUIRED;

    // 隔离级别设置
    Isolation isolation() default Isolation.DEFAULT;

    // 超时时间 单位秒
    int timeout() default -1;

    // 是否只读事务
    boolean readOnly() default false;

    // 指定异常回滚,默认所有异常都回滚
    Class<? extends Throwable>[] rollbackFor() default {};

    // 指定异常名称回滚,~
    String[] rollbackForClassName() default {};

    // 指定发送哪些异常不回滚,~
    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

1.1 Spring 事务管理器

在Spring中,事务管理器的顶层接口是PlatformTransactionManager,当我们在Spring boot里面引入MyBatis时,就会自动创建一个 DataSourceTransactionManager 对象,作为事务管理器

查看PlatformTransactionManager源码

public interface PlatformTransactionManager extends TransactionManager {
    // 获取事务,还可以设置数据属性
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

    // 提交事务
    void commit(TransactionStatus var1) throws TransactionException;

    // 回滚事务
    void rollback(TransactionStatus var1) throws TransactionException;
}

Spring 在事务管理时,就会将这些方法按照约定织入对应的流程中。TransactionDefinition 是一个事务定义器,依赖于 @Transactional 的配置项生成,通过它可以设置事务的属性

二、隔离级别

隔离级别是数据库的概念。场景:对于商品库存,时刻都是多个线程共享的数据,这样就会在多线程环境下扣减商品库存。对于数据库而已,则出现多个事务同时访问同一记录的情况,就会引起数据出现不一致的情况,便是数据库的丢失更新问题

2.1 数据库事务的4个特性
  • 原子性(Atomicity):事务是最小的执行单位,不可分割。事务中的一系列操作要么都成功,要么都失败。
  • 一致性(Consistency):事务前后,数据保持一致。比如,转账业务中,2个人的总额在事务前后保持不变
  • 隔离性(Isolation):并发事务中,一个事务不会影响到其他事务的执行,不会互相干扰
  • 持久性(Durability):事务对数据的修改是永久的,不会因为数据库的故障而改变
2.2 事务隔离级别
  1. 读未提交:允许读取到其他事务尚未提交的数据变更,可能导致脏读,丢失修改,不可重复读,幻读
  2. 读已提交:只允许读取到已经提交的数据变更,防止了脏读,不可重复读,幻读仍存在
  3. 可重复读:在一个事务中,对同一数据的多次读取结果是一致的。除非数据被事务本身所修改
  4. 串行化:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。最高级的隔离级别,可以防止幻读
2.3 使用隔离级别

查看隔离级别源码

package org.springframework.transaction.annotation;

import org.springframework.transaction.TransactionDefinition;

public enum Isolation {

    // 默认隔离级别(使用基础数据存储的默认隔离级别。 所有其他级别对应于JDBC隔离级别。
	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

    // 读未提交
	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
    // 读已提交
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
    // 可重复读
	REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
    // 串行化
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);

	private final int value;

	Isolation(int value) {
		this.value = value;
	}
	public int value() {
		return this.value;
	}
}

三、传播行为

传播行为是方法之间调用事务采取的策略问题。

大多数情况下,我们会认为数据库事务要么全部成功,要么都失败。但也有特殊情况,比如,执行一个批量程序,它会处理很多的交易,绝大多数交易可以顺序完成,但极少数交易可能出现问题,我们不能因为这几个异常交易而回滚整个批量任务。而是只回滚那些出现异常的交易

3.1 传播行为的定义
事务行为说明
REQUIRED默认传播行为,如果当前存在事务,就沿用当前事务。否则新建一个事务运行子方法
SUPPORTS支持当前事务,如果当前没有事务,则将继续采用无事务方式运行子方法
MANDATORY必须使用事务,如果当前没有事务,就抛出异常
REQUIRES_NEW无论当前是否存在事务,都新建事务运行方法。如果当前存在事务,把当前事务挂起
NOT_SUPPORTED不支持事务,如果当前存在事务,就挂起事务,运行方法
NEVER不支持事务,如果当前存在事务,则抛出异常
NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作。

Spring中,是通过枚举类 Propagation定义的,

public enum Propagation {

	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

	NEVER(TransactionDefinition.PROPAGATION_NEVER),

	NESTED(TransactionDefinition.PROPAGATION_NESTED);

	private final int value;

	Propagation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}
}
3.2 测试传播行为-REQUIRED

REQUIRED:默认传播行为,如果当前存在事务,就沿用当前事务,否则新建一个事务运行子方法

示例:批量插入用户

1.项目结构:

在这里插入图片描述

2.UserService接口,和批量操作接口 UserBatchService

public interface UserService {
    int insertUser(User user);
}

// 批量操作接口
public interface UserBatchService {
    int insertUsers(List<User> users);
}

3.对应实现类

// UserService
@Service
public class UserServieImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    // 开启事务,其他默认
    @Transactional
    @Override
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }
}


// UserBatchService实现类
@Service
public class UserBatchServieImpl implements UserBatchService {

    @Autowired
    private UserService userService;

    // 开启事务,隔离级别=读已提交、传播行为=沿用当前事务(当前事务存在的话
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    @Override
    public int insertUsers(List<User> users) {
        final int[] count = {0};
        users.forEach(user-> {
            count[0] += userService.insertUser(user);
        });
        return count[0];
    }
}

4.测试方法

    @Test
    public void testBatchUser(){
        User user = new User();
        user.setUsername("Tom2");
        user.setAddress("北京");

        User user2 = new User();
        user2.setUsername("Cat2");
        user2.setAddress("上海");

        List<User> list = new ArrayList<>();
        list.add(user);
        list.add(user2);

        userBatchService.insertUsers(list);
    }

5.查看输出日志

在这里插入图片描述

从中可以观察出,当调用子方法 insertUser() 时,会加入已经存在的事务。这就是 Propagation.REQUIRED 隔离级别。

3.2 测试传播行为-REQUIRES_NEW

REQUIRES_NEW:无论当前事务是否存在,都会创建新事务运行方法

1.改变 UserServiceImpl的事务设置

@Service
public class UserServieImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    // 设置隔离级别=读已提交 、传播行为=REQUIRES_NEW
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    @Override
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }
}

在这里插入图片描述

2.从日志可以看出,对于子方法 insertUser() 调用时,会开启一个新的事务,独立提交,完全脱离原有事务的管控,每一个事务都有自己独立的隔离级别和锁

3.3 测试传播行为-NESTED

NESTED:在当前方法调用子方法时,如果子方法发生异常,只回滚子方法执行过的SQL,而不回滚当前方法的事务

1.改变 UserSeviceImpl 的事务设置

@Service
public class UserServieImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    // 设置隔离级别=读已提交 、传播行为=REQUIRES_NEW
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.NESTED)
    @Override
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }
}

在这里插入图片描述

从日志中可以看出,当调用 insertUser() 时,就会创建 nested类型事务

3.4 NESTED传播行为和REQUIRES_NEW区别

NESTED 传播行为会沿用当前事务的隔离级别和锁等特性,而REQUIRES_NEW 则可以拥有自己独立的隔离级别和锁等特性。

四、@Transactional 自调用失效问题

4.1 什么是自调用失效问题

之前测试传播行为时,使用的是 UserBatchServiceImpl 去调用 UserService的方法去完成批量导入用户,如果我们不使用 UserBatchService,而把 insertUsers() 也放在 UserServie里面。让它调用本类和 insertUser() 会怎么呢?就像这样

@Service
public class UserServieImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    // 设置隔离级别=读已提交 、传播行为=REQUIRES_NEW
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    @Override
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }

    // 批量插入接口
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    @Override
    public int insertUsers(List<User> users) {
        final int[] count = {0};
        users.forEach(user-> {
            count[0] += this.insertUser(user);
        });
        return count[0];
    }
}

在这里插入图片描述

从日志可以看出,调用 insertUser() 时,没有开启事务。所以该方法上的 @Transactional 注解失效了

4.2 失效的原因

因为 Spring数据库事务,底层实现是 AOP,AOP的原理又是动态代理。

自调用过程中,是类的自身调用,而不是代理对象去调用,就没有使用 AOP ,所以注解失效。

4.3 如何解决?

1.第一种:使用2个 Service,像上面一样。UserBatchService 去调用 UserService。

2.第二种:从容器里面去获取代理对象去启用AOP,就像下面这样

@Service
public class UserServieImpl implements UserService , ApplicationContextAware {

    @Autowired
    private UserMapper userMapper;

    private ApplicationContext applicationContext;

    // 实现生命周期方法,设置IOC容器
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // 单条插入
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    @Override
    public int insertUser(User user) {
        return userMapper.insertUser(user);
    }

    // 批量插入
    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    @Override
    public int insertUsers(List<User> users) {

        // 从容器中获取代理对象
        UserService userService = applicationContext.getBean(UserService.class);

        final int[] count = {0};
        users.forEach(user-> {
            // 使用代理对象插入用户,AOP生效。事务开启成功
            count[0] += userService.insertUser(user);
        });
        return count[0];
    }
}

在这里插入图片描述

参考:《深入浅出Spring Boot 2.x》

;