【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 事务隔离级别
- 读未提交:允许读取到其他事务尚未提交的数据变更,可能导致脏读,丢失修改,不可重复读,幻读
- 读已提交:只允许读取到已经提交的数据变更,防止了脏读,不可重复读,幻读仍存在
- 可重复读:在一个事务中,对同一数据的多次读取结果是一致的。除非数据被事务本身所修改
- 串行化:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。最高级的隔离级别,可以防止幻读
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》