SpringBoot中如何手动开启数据库事务

文章目录

概要

某些情况下我们可能需要手动开启事务,比如由多个业务组合的功能,其中某一段业务报错我们需要进行回滚操作,或者是使用数据库事务实现分布式锁。那么该如何开启事务呢。

开启事务

方式一 :使用@Transactional注解,Spring会自动帮我们管理事务,包括开启事务、提交事务、回滚事务。
方式二 :从数据源DataSource中获取一个Connection,DataSource是自动装配的,SpringBoot默认使用的是HikariDataSource。将Connection自动提交设置为false,用此Connection执行业务SQL,然后提交事务、回滚事务。
方式三 :借助Spring中的事务管理器PlatformTransactionManager来开启事务。
方式四:使用TransactionTemplate,调用其execute方法来执行业务逻辑。

PlatformTransactionManager

方式一是自动管理事务。方式二虽然能手动管理事务,但实际操作起来不太优雅。方式四本质上还是方式三只不过把开启事务、提交事务、回滚事务做了封装,通过lambda函数回调执行我们的业务,可以认为还是帮我们自动管理了事务,这里重点介绍方式三。

先看一段代码和运行效果

java 复制代码
    @Resource
    private UserMapper userMapper;

    @Resource
    private PlatformTransactionManager transactionManager;

    @GetMapping(value = "/transaction", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> transaction() throws InterruptedException {
        TransactionStatus transaction = null;
        try {
            //开启事务
            transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
            User user = userMapper.findById(1L);
            System.out.println("更新前:" + user);

            user.setAge(28);
            userMapper.updateById(user);

            //事物未提交前其他线程读取数据
            Thread otherThread = new Thread(() -> {
                User newUser = userMapper.findById(1L);
                System.out.println("新线程获取更新后的值:" + newUser);
            });
            otherThread.start();
            otherThread.join();

            User newUser = userMapper.findById(1L);
            System.out.println("更新后:" + newUser);
        } finally {
            if (transaction != null) {
                //提交事务
                transactionManager.commit(transaction);
                //其他线程读取事务提交后的值
                Thread otherThread = new Thread(() -> {
                    User user = userMapper.findById(1L);
                    System.out.println("新线程获取事务提交后的值:" + user);
                });
                otherThread.start();
                otherThread.join();
            }
        }
        return ResponseEntity.ok("transaction");
    }

这里使用mybatis来作为持久层框架,PlatformTransactionManager系统已经自动装配,这里直接注入就可以使用。从运行效果来看手动开启的事务是生效的

上面的测试代码是开启了一个新线程来观察事务开启后的效果,由于是新线程必然和当前线程是不会共享事务。但是这种写法需要额外的线程来操作,下面是用mybatis的SqlSessionFactory来开启一个新的SqlSession和当前线程不共享事务。

java 复制代码
    @Resource
    private UserMapper userMapper;

    @Resource
    private PlatformTransactionManager transactionManager;
    
    @Resource
    private SqlSessionFactory sqlSessionFactory;

    @GetMapping(value = "/transaction2", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> transaction2() {
        TransactionStatus transaction = null;
        DefaultSqlSession sqlSession = null;
        try {
            //开启事务
            transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
            User user = userMapper.findById(1L);
            System.out.println("更新前:" + user);

            user.setAge(28);
            userMapper.updateById(user);

            //重新开启一个连接
            Configuration configuration = sqlSessionFactory.getConfiguration();
            sqlSession = new DefaultSqlSession(
                    configuration,
                    configuration.newExecutor(
                            new JdbcTransaction(configuration.getEnvironment().getDataSource().getConnection()),
                            ExecutorType.SIMPLE),
                    true);

            User user2 = sqlSession.getMapper(UserMapper.class).findById(1L);
            System.out.println("新SqlSession获取更新后的值:" + user2);

            User newUser = userMapper.findById(1L);
            System.out.println("更新后:" + newUser);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            if (transaction != null) {
                //提交事务
                transactionManager.commit(transaction);
                if (sqlSession != null) {
                    User newUser = sqlSession.getMapper(UserMapper.class).findById(1L);
                    System.out.println("新SqlSession获取事务提交后的值:" + newUser);
                    sqlSession.close();
                }
            }
        }
        return ResponseEntity.ok("transaction2");
    }

从运行结果来看,事务提交后新sqlSession获取的age应该为28,但仍然是18。这是因为同一个sqlSession执行了相同的查询sql语句时,后续的查询会从缓存中拿值,我们需要在相应的mapper方法上加上@Options注解每次查询前会清空缓然后走数据库查询。

java 复制代码
    @Options(flushCache = Options.FlushCachePolicy.TRUE)
    @Select("select * from user where id = #{id}")
    User findById(Long id);

技术细节

PlatformTransactionManager是如何实现手动管理事务的

PlatformTransactionManager的实现是JdbcTransactionManager,参考DataSourceTransactionManagerAutoConfiguration自动装配类。如果引入了其他事务框架,如spring-boot-starter-data-jpa,那么PlatformTransactionManager实现会是JpaTransactionManager,可以参考HibernateJpaAutoConfiguration自动装配类。不管是JdbcTransactionManager还是JpaTransactionManager在开启事务时做的相关操作都是类似的,都是从数据源中获取到一个新的Connection后将其自动提交设置为false。

当我们在代码中执行

java 复制代码
transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());

getTransaction方法在其抽象类AbstractPlatformTransactionManager中,源码如下

java 复制代码
	public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
			throws TransactionException {

		// 省略相关代码。。。

		// No existing transaction found -> check propagation behavior to find out how to proceed.
		if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
			throw new IllegalTransactionStateException(
					"No existing transaction found for transaction marked with propagation 'mandatory'");
		}
		else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
				def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
				def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
			SuspendedResourcesHolder suspendedResources = suspend(null);
			if (debugEnabled) {
				logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
			}
			try {
				//开启新的事务
				return startTransaction(def, transaction, false, debugEnabled, suspendedResources);
			}
			catch (RuntimeException | Error ex) {
				resume(null, suspendedResources);
				throw ex;
			}
		}
		else {
			// Create "empty" transaction: no actual transaction, but potentially synchronization.
			// 省略相关代码。。。
		}
	}
	private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
			boolean nested, boolean debugEnabled, @Nullable SuspendedResourcesHolder suspendedResources) {

		boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
		DefaultTransactionStatus status = newTransactionStatus(
				definition, transaction, true, newSynchronization, nested, debugEnabled, suspendedResources);
		this.transactionExecutionListeners.forEach(listener -> listener.beforeBegin(status));
		try {
			//此处由实现类实现
			doBegin(transaction, definition);
		}
		catch (RuntimeException | Error ex) {
			this.transactionExecutionListeners.forEach(listener -> listener.afterBegin(status, ex));
			throw ex;
		}
		prepareSynchronization(status, definition);
		this.transactionExecutionListeners.forEach(listener -> listener.afterBegin(status, null));
		return status;
	}

doBegin是抽象方法,其实现在JdbcTransactionManager的父类DataSourceTransactionManager中实现。

java 复制代码
	protected void doBegin(Object transaction, TransactionDefinition definition) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
		Connection con = null;

		try {
			if (!txObject.hasConnectionHolder() ||
					txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
				//从数据源中获取一个新的连接
				Connection newCon = obtainDataSource().getConnection();
				if (logger.isDebugEnabled()) {
					logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
				}
				if (definition.isReadOnly()) {
					checkDefaultReadOnly(newCon);
				}
				// 把新的数据库连接绑定到ConnectionHolder中
				txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
			}

			txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
			con = txObject.getConnectionHolder().getConnection();

			// 省略部分代码。。。
			
			// 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()) {
				txObject.setMustRestoreAutoCommit(true);
				if (logger.isDebugEnabled()) {
					logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
				}
				//将数据库连接改为手动提交
				con.setAutoCommit(false);
			}

			// 省略部分代码。。。

			// Bind the connection holder to the thread.
			if (txObject.isNewConnectionHolder()) {
				//给当前线程的数据源绑定一个ConnectionHolder
				TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
			}
		}

		catch (Throwable ex) {
			// 省略部分代码。。。
		}
	}

源码中可以看到新获取的连接其自动提交被设置为false这样就能实现手动提交事务了。且新的连接被TransactionSynchronizationManager(事务同步器)绑定到当前线程中,事务同步器在绑定数据时是用ThreadLocal来实现的,方便后续线程能直接拿到绑定的数据库连接。

当使用mybatis的mapper接口或者sqlSession查询以及更新数据时,是如何共享事务的。

mapper接口会变成一个代理对象(是一个MapperFactoryBean属于工厂Bean),sql的执行是交给代理对象中封装的sqlSession来完成操作。sqlSession在执行sql语句时最终会交给Executor。

Executor中会有个事务字段transaction是一个接口。在Spring环境下它的实现是SpringManagedTransaction。Executor执行sql语句时会从transaction中获取一个数据连接。

java 复制代码
  public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }
  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

    LOGGER.debug(() -> "JDBC Connection [" + this.connection + "] will"
        + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
  }

可以看到连接的获取是通过工具类DataSourceUtils来操作完成的,这个是spring jdbc中所提供的工具类。

java 复制代码
	public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
		try {
			return doGetConnection(dataSource);
		}
		catch (SQLException ex) {
			throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
		}
		catch (IllegalStateException ex) {
			throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", ex);
		}
	}
    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");

        //这里能看到连接是从事务同步器中拿的
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }
            return conHolder.getConnection();
        }
        // Else we either got no holder or an empty thread-bound holder here.

        // 省略相关代码。。。

        return con;
    }

由于前面开启事务时已经给当前线程绑定了一个ConnectionHolder,这里就直接接能获取到,这样就实现了同一个线程中数据库连接的共享。最后提交事务时是交给doCommit方法完成的。

java 复制代码
	protected void doCommit(DefaultTransactionStatus status) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		// 最开始创建一个新的事务时txObject中已经绑定了ConnectionHolder
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Committing JDBC transaction on Connection [" + con + "]");
		}
		try {
			con.commit();
		}
		catch (SQLException ex) {
			throw translateException("JDBC commit", ex);
		}
	}

事务提交之后从数据源中拿到的Connection自动提交要恢复为true。JdbcTransactionManager的操作是在父类DataSourceTransactionManager的doCleanupAfterCompletion方法中完成的。

java 复制代码
	protected void doCleanupAfterCompletion(Object transaction) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;

		// Remove the connection holder from the thread, if exposed.
		if (txObject.isNewConnectionHolder()) {
			TransactionSynchronizationManager.unbindResource(obtainDataSource());
		}

		// Reset connection.
		Connection con = txObject.getConnectionHolder().getConnection();
		try {
			if (txObject.isMustRestoreAutoCommit()) {
			  // 恢复为自动提交
				con.setAutoCommit(true);
			}
			DataSourceUtils.resetConnectionAfterTransaction(con,
					txObject.getPreviousIsolationLevel(),
					(txObject.isReadOnly() && !isDefaultReadOnly()));
		}
		catch (Throwable ex) {
			logger.debug("Could not reset JDBC Connection after transaction", ex);
		}
		// 省略部分代码。。。
	}

总结

为何mybatis的sqlSession在执行同一个查询sql语句时后续会从缓存中拿值。前面说到sql语句的执行会交给Executor,其查询方法如下。

java 复制代码
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // 如果设置了强制刷新缓存,每次执行查询时都会清空一遍缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 如果从缓存中拿到了值就不从数据库中查询了
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

mybatis在扫描mapper接口时会默认让查询语句的刷新缓存为都为false,这里其实就是mybatis的一级缓存,属于会话级别。当指定Options注解且其flushCache为true时会设置查询语句要刷新缓存,如果是使用xml写sql语句,相应的select标签上指定flushCache属性为true。

相关推荐
05大叔2 小时前
Spring Day02
数据库·sql·spring
默默前行的虫虫2 小时前
nicegui中多次调用数据库操作总结
数据库·python
鸽鸽程序猿2 小时前
【Redis】事务
数据库·redis·缓存
Knight_AL3 小时前
MySQL 分区表应用案例:优化数据管理与性能
数据库·mysql
墨着染霜华3 小时前
Spring Boot整合Kaptcha生成图片验证码:新手避坑指南+实战优化
java·spring boot·后端
我爱学习好爱好爱3 小时前
Prometheus监控栈 监控java程序springboot
java·spring boot·prometheus
老华带你飞3 小时前
考试管理系统|基于java+ vue考试管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
九皇叔叔3 小时前
MySQL 数据库 MVCC 与锁如何联手解决脏读、不可重复读、幻读
数据库·mysql
WZTTMoon3 小时前
Spring Boot OAuth2 授权码模式开发实战
大数据·数据库·spring boot