水煮MyBatis(十一)- 事务
前言
在springboot中,如果需要启用事务,有两个核心注解是不能缺失的:
- @EnableTransactionManagement 在Application上添加;
- @Transactional 在方法或者类上添加;
在这里以MySQL举例
事务语句
一般涉及到的语句主要有下面几个:
- BEGIN :显式地开启一个事务,也可以使用START TRANSACTION;
- COMMIT:提交事务,并使已对数据库进行的所有修改成为永久性的;
- ROLLBACK: 结束用户的事务,并撤销正在进行的所有未提交的修改;
- SET TRANSACTION :设置事务的隔离级别。InnoDB 存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE。
其中还有一个保存点的概念【SAVEPOINT】,在业务代码中比较少见,这里就不介绍了。
Mybatis事务类图
简要说明:
- SpringManagedTransaction:看名称就知道,在Spring体系中一般使用此实现。代理 JDBC 连接的生命周期。 它从 Spring 的事务管理器中获取连接,结束后归还。如果 Spring 的事务处理处于活动状态,它将不操作所有提交/回滚/关闭调用,假设 Spring 事务管理器将完成这项工作。如果不是,则与JdbcTransaction的处理方式一致。
- JdbcTransaction:直接使用 JDBC 的conn提交和回滚。它依赖从 dataSource 获取到的连接来管理事务的范围。延迟连接获取,直到调用 getConnection()。启用自动提交时忽略提交或回滚请求。
- ManagedTransaction:让容器管理事务的整个生命周期。延迟连接获取,直到调用 getConnection()。忽略所有提交或回滚请求。默认情况下,它会关闭连接,但可以配置为不这样做。
相关源码
本来想介绍JdbcTransaction的,一来ManagedTransaction很少用到,二来在Spring体系中,一般直接使用SpringManagedTransaction,所以这里就以SpringManagedTransaction为主来展开说明了。
开始和关闭
每开启一个事务,都会创建SpringManagedTransaction,对象中的其余几个属性,在获取链接时再进行初始化。
public SpringManagedTransaction(DataSource dataSource) {
this.dataSource = dataSource;
}
// 打开数据库链接
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
// 是否以事务的方式打开链接
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
}
ConnectionTransactional: Determine whether the given JDBC Connection is transactional, that is,bound to the current thread by Spring's transaction facilities. 判定给定的jdbc链接是否存在事务,如果是,则把Srping事务管理器绑定到当前线程。
事务初始化里,需要注意的是spring会将autoCommit属性自动设置为false,然后归还链接之前,恢复原始设置。autoCommit默认为true。 相关类:DataSourceTransactionManager
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
...
// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
// 设置标记,在归还conn时,将AutoCommit设置为true
txObject.setMustRestoreAutoCommit(true);
con.setAutoCommit(false);
}
...
}
注意那行英文注释:
手动提交的成本是比较高的,如果没有什么必要,还是用自动提交。
@Override
protected void doCleanupAfterCompletion(Object transaction) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
// Reset connection.
Connection con = txObject.getConnectionHolder().getConnection();
if (txObject.isMustRestoreAutoCommit()) {
// 重置自动提交
con.setAutoCommit(true);
}
// 重置隔离级别和只读标记
DataSourceUtils.resetConnectionAfterTransaction(
con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
// 重置一些设置,比如保存点、事务、回滚等标记
txObject.getConnectionHolder().clear();
}
主要有三件事
- 如果之前有修改过autoCommit设定,这里需要重置;
- 重置隔离级别和只读标记
- 重置一些常用设置,比如保存点、事务、回滚等标记
提交和回滚
从代码中可以看到,提交和回滚的前置条件都是一样,即:
- 开启事务;
- 关闭自动提交;
- 链接非空;
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
this.connection.commit();
}
}
public void rollback() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
this.connection.rollback();
}
}
抛开最后一个条件不说,如果一个链接没有开启事务,手动调用commit的时候,也会报错的。我们来看MySQL驱动中,ConnectionImpl类对commit的实现:
public void commit() throws SQLException {
if (this.connectionLifecycleInterceptors != null) {
// 执行链接生命周期拦截器,忽略不看
}
// 如果是自动提交,则报错
if (this.autoCommit && !getRelaxAutoCommit()) {
throw SQLError.createSQLException("Can't call commit when autocommit=true", getExceptionInterceptor());
} else if (this.transactionsSupported) {
// 执行提交语句
execSQL(null, "commit", -1, null, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY, false, this.database, null, false);
}
}
connection执行提交的时候,就做了这些事情
- 执行链接生命周期拦截器;
- 如果是自动提交,则抛出异常;
- 执行提交语句;
转载自:https://juejin.cn/post/7244339465559425079