目录
1. Spring事务分类
分为编程式事务,声明式事务
编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚
声明式事务将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。
2. 声明式事务
要想实现事务管理和业务代码的抽离,就必须得用到 Spring 当中的AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。
2.1 @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;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
2.2 添加位置
接口实现类或接口实现方法上,而不是接口类中。将标签放置在需要进行事务管理的方法上,而不是放在所有接口实现类上,例如只读的就不需要
2.3 属性配置
1. value :主要用来指定不同的事务管理器;主要用来满足在同一个系统中,存在不同的事务管理器。比如在Spring中,声明了两种事务管理器txManager1, txManager2.然后,用户可以根据这个参数来根据需要指定特定的txManager. 2. value 适用场景:在一个系统中,需要访问多个数据源或者多个数据库,则必然会配置多个事务管理器的 3. rollbackFor:让受检查异常回滚;即让本来不应该回滚的进行回滚操作。 4. noRollbackFor:忽略非检查异常;即让本来应该回滚的不进行回滚操作。
2.4 事务传播
当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播,例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。
声明式事务的传播行为可以通过 @Transactional 注解中的 propagation 属性来定义,比如说:
@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}
TransactionDefinition 一共定义了 7 种事务传播行为,其中PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW 两种传播行为是比较常用的。
PROPAGATION_REQUIRED
这也是 @Transactional 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:
如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。 如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。
也就是说如果a方法和b方法都添加了注解,在默认传播模式下,a方法内部调用b方法,会把两个方法的事务合并为一个事务。
演示1:
@Transactional(rollbackFor=Exception.class,propagation= Propagation.REQUIRED)
public void methodM() {
insertTableA();
methodN();
insertTableA();
}
@Transactional(rollbackFor=Exception.class,propagation= Propagation.REQUIRED)
public void methodN() {
insertTableB();
throw new RuntimeException();
}
结果:
A表、B表均插入失败。 methodM方法存在事务,methodN方法直接加入到methodM方法的事务中,所以methodN抛出异常,methodM方法回滚。
演示2:
public void methodM() {
insertTableA();
methodN();
}
@Transactional(rollbackFor=Exception.class,propagation= Propagation.REQUIRED)
public void methodN() {
insertTableB();
throw new RuntimeException();
}
结果:
A表插入成功,B表插入失败。 methodM方法不存在事务,methodN方法新建一个事务。
PROPAGATION_REQUIRES_NEW
说明:新建事务,如果当前存在事务,把当前事务挂起。
内部的事务独立运行,在各自的作用域中,可以独立的回滚或者提交; 而外部的事务将不受内部事务的回滚状态影响
@Transactional(rollbackFor=Exception.class,propagation= Propagation.REQUIRED)
public void methodM() {
insertTableA();
methodN();
throw new RuntimeException();
}
@Transactional(rollbackFor=Exception.class,propagation= Propagation.REQUIRES_NEW)
public void methodN() {
insertTableB();
}
结果:
A表插入失败,B表插入成功。 methodN方法直接新建事务,methodM中抛出异常不影响methodN事务执行。
2.5 事务隔离级别
-
ISOLATION_DEFAULT: 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.另外四个与JDBC的隔离级别相对应。
-
ISOLATION_READ_UNCOMMITTED:允许许令外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
-
ISOLATION_READ_COMMITTED: 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。
-
ISOLATION_REPEATABLE_READ: 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。
-
ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。
@Transactional(isolation = Isolation.DEFAULT)
查看mysql数据库隔离级别:SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;
2.6 事务超时时间
timeout事务超时时间,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。业务场景:假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源。默认是:-1(表示没有限制最长时间)
2.7 事务的回滚策略
回滚策略rollbackFor,用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。默认情况下,事务只在出现运行时异常(Runtime Exception)或者 Error时回滚,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。
2.8 事务的只读属性
事务的只读属性readOnly, 如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。默认为读写事务。
为什么一个查询操作还要启用事务支持呢?
这是因为 MySql(innodb)默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySql 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。那如果我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。 有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。
spring doc 有如下描述:
Read-only status: A read-only transaction can be used when your code reads but does not modify data. Read-only transactions can be a useful optimization in some cases, such as when you are using Hibernate.
只读状态:当代码读取但不修改数据时,可以使用只读事务。在某些情况下,只读事务可能是一种有用的优化,例如当您使用hibernate时
只读事务
并不是一个强制选项,它只是一个“暗示”,提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。 但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。因此,“只读事务”仅仅是一个性能优化的推荐配置而已,并非强制你要这样做不可
3. 编程式事务
编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。
在有些场景下,我们需要获取事务的状态,是执行成功了还是失败回滚了,那么使用声明式事务就不够用了,需要编程式事务。
3.1 TransactionTemplate
借助(TransactionCallback)执行事务管理,带有返回值:
public Object getObject(String str) {
/*
* 执行带有返回值<Object>的事务管理
*/
transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
try {
...
//....... 业务代码
return new Object();
} catch (Exception e) {
//回滚
transactionStatus.setRollbackOnly();
return null;
}
}
});
}
借助(TransactionCallbackWithoutResult)执行事务管理,无返回值:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}
3.2 TransactionManager
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
可以把事务结果同步返回给调用端,出异常返回false,成功返回true。
我们就基于这种方法来做一个工具类。这个工具类作用是接收一个Service层需要被事务包围的方法为参数,然后给调用端返回事务结果,供调用端根据结果做相应的处理。
@Slf4j
@Component
public class TransactionUtil {
@Autowired
private PlatformTransactionManager transactionManager;
public <T> boolean transactional(Consumer<T> consumer) {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
consumer.accept(null);
transactionManager.commit(status);
return true;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("编程式事务业务异常回滚", e);
return false;
}
}
}
Service举例:
@Service
public class TestService {
/**
* 此处不需要事务,由TransactionUtil控制事务
*/
public void doSome(int i) {
System.out.println("我是Service层" + i);
}
}
Controller中就可以使用
// 获取事务的执行结果
boolean result = transactionUtil.transactional(s -> testService.doSome(1))
3.3 总结
就编程式事务管理而言,Spring 更推荐使用 TransactionTemplate
声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
因为声明式事务是通过注解的,有些时候还可以通过配置实现,这就会导致一个问题,那就是这个事务有可能被开发者忽略。
-
事务被忽略了有什么问题呢?
首先,如果开发者没有注意到一个方法是被事务嵌套的,那么就可能会再方法中加入一些如RPC远程调用、消息发送、缓存更新、文件写入等操作。
我们知道,这些操作如果被包在事务中,有两个问题:
1、这些操作自身是无法回滚的,这就会导致数据的不一致。可能RPC调用成功了,但是本地事务回滚了,可是PRC调用无法回滚了。
2、在事务中有远程调用,就会拉长整个事务。那么久会导致本事务的数据库连接一直被占用,那么如果类似操作过多,就会导致数据库连接池耗尽。
但是如果是编程式事务的话,业务代码中就会清清楚楚看到什么地方开启事务,什么地方提交,什么时候回滚。这样有人改这段代码的时候,就会强制他考虑要加的代码是否应该方法事务内。
4. 事务失效的场景
4.1 访问权限
如果我们自定义的事务方法(即目标方法),它的访问权限不是public
,而是private、default或protected的话,spring则不会提供事务功能
在AbstractFallbackTransactionAttributeSource
类的computeTransactionAttribute
方法中有个判断,如果目标方法不是public,则TransactionAttribute
返回null,即不支持事务
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
4.2 方法用final修饰
spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。如果某个方法是static的,同样无法通过动态代理,变成事务方法。
我自己测了好像也生效
4.3 方法内部调用
接口中A、B两个方法,A无@Transactional标签,B有,上层通过A间接调用B,此时事务不生效。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}
4.4 未被spring管理
//@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
4.5 多线程调用
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
如果方法中调用多线程,方法上的事务不会传递到线程中,就是当前方法的注解只能保证该方法内的事务,线程中出现任何异常都不影响外面的事务,
4.6 表不支持事务
低版本的MySQL数据库引擎是myisam,不支持事务
4.7 未开启事务
事务相关配置信息未配置
springboot通过DataSourceTransactionManagerAutoConfiguration
类,已经默默的帮你开启了事务。你所要做的事情很简单,只需要配置spring.datasource
相关参数即可。
4.8 自己吞了异常
事务不会回滚,最常见的问题是:开发者在代码中手动try…catch了异常,对异常进行处理,spring认为程序正藏
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
4.9 不符合事务处理的异常类型
因为spring事务,默认情况下只会回滚RuntimeException
(运行时异常)和Error
(错误),对于普通的Exception(非运行时异常),它不会回滚。一般情况下,将rollbackFor该参数设置成:Exception或Throwable。
5. 其他
5.1 大事务问题
通常情况下,我们会在方法上@Transactional
注解,填加事务功能,比如:
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
query1();
query2();
query3();
roleService.save(userModel);
update(userModel);
}
}
@Service
public class RoleService {
@Autowired
private RoleService roleService;
@Transactional
public void save(UserModel userModel) throws Exception {
query4();
query5();
query6();
saveData(userModel);
}
}
但@Transactional
注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。上面的这个例子中,在UserService类中,其实只有这两行才需要事务:
roleService.save(userModel);
update(userModel);
在RoleService类中,只有这一行需要事务:
saveData(userModel);
解决:
-
少用@Transactional注解
-
将查询(select)方法放到事务外,编程式事务
-
事务中避免远程调用
-
事务中避免一次性处理太多数据
-
异步处理,有些不需要事务的异步处理
JDBC处理事务
Connection connection = null;
PreparedStatement pstmt = null;
ResultSet resultSet = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/dbname?characterEncoding=utf-8","username", "password");
connection.setAutoCommit(false);
// others ......
connection.commit();
} catch (Exception e) {
connection.rollback();
} finally {
connection.setAutoCommit(true);
// close connection
}