Spring事务能更改数据库的隔离级别么?详细解析spring事务各种问题spring事务提供了事务传播行为、事务隔离级
问题
为了帮助大家对spring事务的了解,大家可以带着下面的问题来看这篇文章。
- 什么是事务?
- Spring事务都有哪些功能?
- Spring事务隔离级别会覆盖数据库的隔离级别么?
- Spring事务的
NESTED
级别和REQUIRED
离级别有什么区别? - 在异步线程中,spring事务还生效么?
介绍
事务
在平时开发过程中,事务对于我来讲就是执行多条sql语句,这些sql语句要么都执行要么都不执行,并且执行完后的数据保证不丢失。它有以下特性(ACID):
- 原子性(Atomicity):事务是最小的执行单位,不可分割。确保要么全部执行,要么都不执行;
- 一致性(Consistency):执行事务前后,数据保持一致,例如转账业务中,无论是否执行成功,转账者和收款人的总额应该是不变的;
- 隔离性(Isolation):并发访问数据库时不被其他事务所干扰,各事务之间数据是完全独立的;
- 持久性(Durability):一个事务被提交之后。数据的更改是持久的,即使数据库发生故障也不应该对其有任何影响。
想了解详细内容可以查看我的mysql专栏。
Spring事务
Spring事务 它是基于AOP(切面编程),使用切面来管理事务的边界;它可以通过定义事务的传播行为、隔离级别等属性,来管理一系列数据库操作的执行方式,方便我们更容易使用它来满足不同应用场景的需求。它主要有两种使用方式:编程式事务管理和声明式事务管理。下面代码示例中我们主要采用声明式事务(@Transactional
)
- 编程式事务:通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。
- 声明式事务:将事务管理代码从业务方法中分离出来,通过aop进行封装。Spring声明式事务能让我们不需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。直接在对应方法上使用 @Transactional 注解就可以开启声明式事务。
使用
在spring中,绝大部分场景都是通过@Transactional
注解来直接使用事务,我们先来看一下这个注解。代码如下:
@Target({ElementType.TYPE, ElementType.METHOD})
@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;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
通过上述代码可以看到ElementType.TYPE, ElementType.METHOD,说明该注解既可以配置在类上又可以配置在方法上。作用在类上时相当于整个类上的方法都加了该注解。 它的属性含义如下:
value
和transactionManager
:这两个属性互为别名。它们用于指定事务管理器的标识或限定符。默认值为空字符串。propagation
:指定事务的传播行为,默认是Propagation.REQUIRED
。isolation
:指定事务的隔离级别,默认是Isolation.DEFAULT
。timeout
:指定事务的超时时间(以秒为单位),默认使用底层事务系统的默认超时。readOnly
:一个布尔标志,指示事务是否为只读,默认值为false
。rollbackFor
:定义一个或多个Throwable
的子类的类数组,这些异常类型会导致事务回滚。rollbackForClassName
:定义一个或多个异常名称(字符串数组),这些异常类型会导致事务回滚。noRollbackFor
:定义一个或多个Throwable
的子类的类数组,这些异常类型不会导致事务回滚。noRollbackForClassName
:定义一个或多个异常名称(字符串数组),这些异常类型不会导致事务回滚。
接下来我们通过代码的方式来设置以上各种参数以便大家更好理解每个参数的含义。
前置准备
首先定义表结构:
-- 用户表
create table temp_user
(
id bigint primary key ,
name varchar(255)
);
-- 用户行为表
create table temp_user_action
(
id bigint primary key ,
user_id bigint,
action smallint
);
底层sql代码我忽略掉了,大家可以自行实现,只提供两个上层方法
@Resource
private TempUserRepo tempUserRepo;
@Resource
private TempUserActionRepo tempUserActionRepo;
@Transactional
public void test() {
tempUserRepo.saveUser(new TempUser(1, "1"));
tempUserActionRepo.saveUserAction(new TempUserAction(1, 1, 1));
}
value和transactionManager
这两个属性使用如下:
@Transactional(value = "transactionManager", transactionManager = "transactionManager")
他们两个可以互相替换,配置的目标值是事务管理器Bean的名字。如果两个字段都配置了,那么配置的值必须相同,如果不同则会抛出异常
attribute 'transactionManager' and its alias 'value' are declared with values of [transactionManager2] and [transactionManager1]
propagation 事务传播行为
使用如下:
在spring中有七种事务传播行为:
REQUIRED:如果当前没有事务,则创建一个新的事务;如果已经存在一个事务,则加入当前事务; SUPPORTS:如果当前有事务,则加入该事务;如果当前没有事务,也可以非事务方式运行; MANDATORY:支持当前事务,如果没有事务则抛出异常; REQUIRES_NEW:每次都创建一个新的事务,如果当前已经有一个事务,则挂起当前事务。挂起当前事务的意思是,当前事务的执行暂停,待新事务完成后再恢复执行。新事务提交后,主事务回滚不会影响这个新事务。 NOT_SUPPORTED:当前方法不支持事务,如果当前有事务,则将事务挂起; NEVER:不能在事务中运行,如果当前有事务,则抛出异常; NESTED:如果当前有事务,则在当前事务中嵌套一个事务,若外部事务回滚,则嵌套事务也会回滚;如果当前没有事务,则创建一个新的事务。
默认传播行为是REQUIRED,根据上述解释大部分都能理解这些传播行为的作用;但其中NESTED比较难理解,其实在外部没有事务时它和REQUIRED是一样的,只有在外部有事务时他们有一些区别,区别点在数据的回滚。下面我举个例子帮助大家理解,代码如下:
外层开启一个事务,执行保存用户,在保存用户行为时开启嵌套事务,在保存数据前抛出一个异常,
@Transactional
public void test() {
tempUserRepo.saveUser(new TempUser(1, "spring事务"));
tempUserActionRepo.saveUserAction(new TempUserAction(1, 1, 1));
}
@Transactional(propagation = Propagation.NESTED)
public void saveUser(TempUser tempUser) {
tempUserMapper.insert(tempUser);
}
@Transactional(propagation = Propagation.NESTED)
public void saveUserAction(TempUserAction tempUserAction) {
tempUserActionMapper.insert(tempUserAction);
try {
int i = 1 / 0;
} catch (Exception e) {
throw new RuntimeException();
}
}
在test
方法中开启事务并调用保存用户和保存用户行为方法,这两个方法都使用嵌套子事务,执行完结果会发现,用户保存成功了,而用户行为保存失败了,虽然他们在同一个事务中,仍然可以做到前面的sql执行成功后面的执行失败,这是因为在保存用户数据时开启了安全点,当事务需要回滚时会回滚到最近的一个安全点,以保证数据的一致性;在mysql中的命令如下(大家可以在自己的数据库中测试一下):
START TRANSACTION; -- 开始一个事务
INSERT INTO temp_user (id, name)
VALUES (1, 'spring事务'); -- 执行一个操作
SAVEPOINT my_savepoint1;
INSERT INTO temp_user (id, name)
VALUES (2, 'spring事务'); -- 执行一个操作
SAVEPOINT my_savepoint2;
INSERT INTO temp_user_action (id, user_id, action)
VALUES (1, 1, 1); -- 执行一个操作
ROLLBACK TO my_savepoint2; -- 回滚到保存点
COMMIT; -- 提交事务
isolation 事务隔离级别
我开始一度怀疑在代码中设置事务隔离级别是无效的,直到我尝试过了一次才确信它是能生效的。根据下面代码我们来演示一下它的效果:
-- 1、命令
SELECT @@GLOBAL.TX_ISOLATION;
-- 结果 : 可重复读
REPEATABLE-READ
-- 2、开启事务
START TRANSACTION;
-- 3、插入数据
insert into temp_user(id, name)
value (1, 'spring事务');
-- 5、提交事务
COMMIT; -- 提交事务
程序代码:
// 4、执行代码
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void test() {
TempUser tempUser = tempUserRepo.getById(1);
log.info("用户为:{}", tempUser);
}
按照上述命令步骤执行:
- 当前数据库的隔离级别是可重复读,此时意味着只有事务提交了数据,其他事务才可能读到该数据;
- 先在数据库中开启一个事务;
- 给用户表插入一条数据;
- 此时在代码中进行查询该数据;
- 提交事务。
在执行第四步的时候程序会打印用户为:TempUser(id=1, name=spring事务)
,说明在spring事务上设置隔离级别也是生效的,此时再次执行命令1
结果还是REPEATABLE-READ,那spring是如何更改实现读未提交
隔离级别的呢?其实在spring中如果设置了事务隔离级别,它会在开启事务之前执行
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;;该命令的作用范围是当前链接作用次数为1次,并且只有在REQUIRED
、REQUIRES_NEW
传播行为下生效。
timeout 超时时间
代码如下:
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 1)
public void test() {
try {
Thread.sleep(3000L);
} catch (Exception e) {
}
tempUserRepo.saveUser(new TempUser(2, "spring事务"));
}
timeout单位为秒,当代码执行超过配置时间会抛出TransactionTimedOutException异常,这里要注意的是:timeout时间指的是方法开始到最后一条sql执行的时间,而不是整个方法的时间。并且只有在REQUIRED
、REQUIRES_NEW
传播行为下生效。
readOnly 只读标识
当对事务设置为readOnly = true时,如果该事务下面有写操作则会抛出Connection is read-only
回滚/不回滚 异常
捕获回滚的异常比较简单,这里就不演示了。这里主要看一下不回滚的异常。代码如下:
@Transactional(noRollbackFor = {SQLIntegrityConstraintViolationException.class})
public void test() {
tempUserRepo.saveUser(new TempUser(2, "spring事务"));
tempUserActionRepo.saveUserAction(new TempUserAction(1, 1, 1));
}
此时数据库中,已经插入了TempUser(1,"spring事务")和TempUserAction(1,1,1)这两条数据,在执行上述代码时保存用户可以执行成功,而用户行为因为主键冲突会抛出SQLIntegrityConstraintViolationException异常,此时我们在不回滚异常里面设置为该异常,事务还是会正常回滚的,因为对于mysql来讲这两个sql是同一个事务,当执行sql出错时是一定会回滚的。这里只能设置非数据库抛出的异常才会生效,例 如下代码:
@Transactional(noRollbackFor = {RuntimeException.class})
public void test() {
tempUserRepo.saveUser(new TempUser(2, "spring事务"));
tempUserActionRepo.saveUserAction(new TempUserAction(2, 2, 2));
try {
int i = 1 / 0;
} catch (Exception e) {
throw new RuntimeException();
}
}
异步线程中设置事务
在异步线程内使用@Transactional
可以保证该方法在同一个事务中执行,但它和主线程不在同一个事务内。代码如下:
@Transactional
public void test() {
tempUserActionRepo.saveUserAction(new TempUserAction(3, 1, 1));
new Thread(
() -> asyncService.asyncMethod()
).start();
tempUserActionRepo.saveUserAction(new TempUserAction(4, 1, 1));
}
@Transactional
public void asyncMethod() {
tempUserRepo.saveUser(new TempUser(1, "spring事务"));
try {
int i = 10 / 0;
} catch (Exception e) {
throw new RuntimeException();
}
tempUserActionRepo.saveUserAction(new TempUserAction(1, 1, 1));
}
最终数据库中的数据为TempUserAction(3, 1, 1)和TempUserAction(4, 1, 1)。看了很多文章都说异步线程中的事务不生效,但我尝试了多种方式都是生效的。
注意事项
spring在实现spring事务时采用的是spring aop代理的方式,当没有一个方法没有被代理时或者无法被代理时就会导致事务生效,这一点要注意,以下是我整理出来一些事务失效的场景:
- 没配置事务管理器;
- 当前service没被spring管理;
- 方法被final、static修饰;
- 在同一个类中内部方法调用;
- 访问权限不是public;
- 数据库不支持事务;
- 异步线程调用方法时,需要在该方法上增加@Transactional注解,会开启新事务;
- 把异常吃掉了,没有抛出来;
总结
事务有ACID的特性;spring事务提供了事务传播行为、事务隔离级别、事务超时时间、只读事务、异常处理功能;这些功能可以让我们更方便的使用事务;Spring通过设置SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; 来覆盖数据库的事务隔离级别;NESTED引入了安全点机制,当数据需要回滚时只需要回滚到最近的安全点位置,而REQUIRED需要全部回滚;在异步线程中使用spring事务会生效。
转载自:https://juejin.cn/post/7405852656755195967