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。

相关推荐
CJi0NG28 分钟前
【自用】MySQL-多表查询
数据库·mysql
雨落在了我的手上34 分钟前
MySQL数据库基础(12):事务
数据库·mysql
D姐聊门店37 分钟前
门店收银系统断网兜底怎么设计:本地账本、离线续传与日终对账的工程实现
数据库·saas
用户8662978480081 小时前
抓包看 KingbaseES 内部协议?官方给了个现成工具
数据库
wefg11 小时前
【Redis】安装与启动
数据库·redis·缓存
大圣编蚕1 小时前
常见的 Redis 面试题(高频整理)
数据库·redis·缓存
派小心.2 小时前
网站统计采样数据的误差核对
大数据·服务器·前端·数据库·人工智能·小程序·数据分析
龙腾AI白云2 小时前
数字孪生、具身智能、世界模型三者交叉
数据库·人工智能·机器学习·flask·pygame
SelectDB技术团队3 小时前
Apache Doris 基于 Lance 的向量检索深度解析
数据库·数据仓库·数据分析·湖仓一体·apache doris·lakehouse·多模湖仓