SpringBoot中手動(dòng)開(kāi)啟數(shù)據(jù)庫(kù)事務(wù)的四種方式
概要
某些情況下我們可能需要手動(dòng)開(kāi)啟事務(wù),比如由多個(gè)業(yè)務(wù)組合的功能,其中某一段業(yè)務(wù)報(bào)錯(cuò)我們需要進(jìn)行回滾操作,或者是使用數(shù)據(jù)庫(kù)事務(wù)實(shí)現(xiàn)分布式鎖。那么該如何開(kāi)啟事務(wù)呢。
開(kāi)啟事務(wù)
方式一:使用@Transactional注解,Spring會(huì)自動(dòng)幫我們管理事務(wù),包括開(kāi)啟事務(wù)、提交事務(wù)、回滾事務(wù)。
方式二:從數(shù)據(jù)源DataSource中獲取一個(gè)Connection,DataSource是自動(dòng)裝配的,SpringBoot默認(rèn)使用的是HikariDataSource。將Connection自動(dòng)提交設(shè)置為false,用此Connection執(zhí)行業(yè)務(wù)SQL,然后提交事務(wù)、回滾事務(wù)。
方式三:借助Spring中的事務(wù)管理器PlatformTransactionManager來(lái)開(kāi)啟事務(wù)。
方式四:使用TransactionTemplate(自動(dòng)注入即可),調(diào)用其execute方法來(lái)執(zhí)行業(yè)務(wù)邏輯。
PlatformTransactionManager
方式一是自動(dòng)管理事務(wù)。方式二雖然能手動(dòng)管理事務(wù),但實(shí)際操作起來(lái)不太優(yōu)雅。方式四本質(zhì)上還是方式三只不過(guò)把開(kāi)啟事務(wù)、提交事務(wù)、回滾事務(wù)做了封裝,通過(guò)lambda函數(shù)回調(diào)執(zhí)行我們的業(yè)務(wù),可以認(rèn)為還是自動(dòng)管理了事務(wù),這里重點(diǎn)介紹方式三。
先看一段代碼和運(yùn)行效果
@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 {
//開(kāi)啟事務(wù)
transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
User user = userMapper.findById(1L);
System.out.println("更新前:" + user);
user.setAge(28);
userMapper.updateById(user);
//事物未提交前其他線程讀取數(shù)據(jù)
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) {
//提交事務(wù)
transactionManager.commit(transaction);
//其他線程讀取事務(wù)提交后的值
Thread otherThread = new Thread(() -> {
User user = userMapper.findById(1L);
System.out.println("新線程獲取事務(wù)提交后的值:" + user);
});
otherThread.start();
otherThread.join();
}
}
return ResponseEntity.ok("transaction");
}
這里使用mybatis來(lái)作為持久層框架,PlatformTransactionManager系統(tǒng)已經(jīng)自動(dòng)裝配,這里直接注入就可以使用。從運(yùn)行效果來(lái)看手動(dòng)開(kāi)啟的事務(wù)是生效的
上面的測(cè)試代碼是開(kāi)啟了一個(gè)新線程來(lái)觀察事務(wù)開(kāi)啟后的效果,由于是新線程必然和當(dāng)前線程是不會(huì)共享事務(wù)。但是這種寫(xiě)法需要額外的線程來(lái)操作,下面是用mybatis的SqlSessionFactory來(lái)開(kāi)啟一個(gè)新的SqlSession和當(dāng)前線程不共享事務(wù)。
@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 {
//開(kāi)啟事務(wù)
transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
User user = userMapper.findById(1L);
System.out.println("更新前:" + user);
user.setAge(28);
userMapper.updateById(user);
//重新開(kāi)啟一個(gè)連接
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) {
//提交事務(wù)
transactionManager.commit(transaction);
if (sqlSession != null) {
User newUser = sqlSession.getMapper(UserMapper.class).findById(1L);
System.out.println("新SqlSession獲取事務(wù)提交后的值:" + newUser);
sqlSession.close();
}
}
}
return ResponseEntity.ok("transaction2");
}
從運(yùn)行結(jié)果來(lái)看,事務(wù)提交后新sqlSession獲取的age應(yīng)該為28,但仍然是18。這是因?yàn)橥粋€(gè)sqlSession執(zhí)行了相同的查詢sql語(yǔ)句時(shí),后續(xù)的查詢會(huì)從緩存中拿值,我們需要在相應(yīng)的mapper方法上加上@Options注解每次查詢前會(huì)清空緩然后走數(shù)據(jù)庫(kù)查詢。
@Options(flushCache = Options.FlushCachePolicy.TRUE)
@Select("select * from user where id = #{id}")
User findById(Long id);
技術(shù)細(xì)節(jié)
PlatformTransactionManager是如何實(shí)現(xiàn)手動(dòng)管理事務(wù)的

PlatformTransactionManager的實(shí)現(xiàn)是JdbcTransactionManager,參考DataSourceTransactionManagerAutoConfiguration自動(dòng)裝配類。如果引入了其他事務(wù)框架,如spring-boot-starter-data-jpa,那么PlatformTransactionManager實(shí)現(xiàn)會(huì)是JpaTransactionManager,可以參考HibernateJpaAutoConfiguration自動(dòng)裝配類。不管是JdbcTransactionManager還是JpaTransactionManager在開(kāi)啟事務(wù)時(shí)做的相關(guān)操作都是類似的,都是從數(shù)據(jù)源中獲取到一個(gè)新的Connection后將其自動(dòng)提交設(shè)置為false。

當(dāng)我們?cè)诖a中執(zhí)行
transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
getTransaction方法在其抽象類AbstractPlatformTransactionManager中,源碼如下
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException {
// 省略相關(guān)代碼。。。
// 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 {
//開(kāi)啟新的事務(wù)
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.
// 省略相關(guān)代碼。。。
}
}
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 {
//此處由實(shí)現(xiàn)類實(shí)現(xiàn)
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是抽象方法,其實(shí)現(xiàn)在JdbcTransactionManager的父類DataSourceTransactionManager中實(shí)現(xiàn)。
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
//從數(shù)據(jù)源中獲取一個(gè)新的連接
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
if (definition.isReadOnly()) {
checkDefaultReadOnly(newCon);
}
// 把新的數(shù)據(jù)庫(kù)連接綁定到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).
// 正常情況下新獲取的連接都是自動(dòng)提交
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
//將數(shù)據(jù)庫(kù)連接改為手動(dòng)提交
con.setAutoCommit(false);
}
// 省略部分代碼。。。
// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
//給當(dāng)前線程的數(shù)據(jù)源綁定一個(gè)ConnectionHolder
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
// 省略部分代碼。。。
}
}源碼中可以看到新獲取的連接其自動(dòng)提交被設(shè)置為false這樣就能實(shí)現(xiàn)手動(dòng)提交事務(wù)了。且新的連接被TransactionSynchronizationManager(事務(wù)同步器)綁定到當(dāng)前線程中,事務(wù)同步器在綁定數(shù)據(jù)時(shí)是用ThreadLocal來(lái)實(shí)現(xiàn)的,方便后續(xù)線程能直接拿到綁定的數(shù)據(jù)庫(kù)連接。
當(dāng)使用mybatis的mapper接口或者sqlSession查詢以及更新數(shù)據(jù)時(shí),是如何共享事務(wù)的。
mapper接口會(huì)變成一個(gè)代理對(duì)象(是一個(gè)MapperFactoryBean屬于工廠Bean),sql的執(zhí)行是交給代理對(duì)象中封裝的sqlSession來(lái)完成操作。sqlSession在執(zhí)行sql語(yǔ)句時(shí)最終會(huì)交給Executor。

Executor中會(huì)有個(gè)事務(wù)字段transaction是一個(gè)接口。在Spring環(huán)境下它的實(shí)現(xiàn)是SpringManagedTransaction。Executor執(zhí)行sql語(yǔ)句時(shí)會(huì)從transaction中獲取一個(gè)數(shù)據(jù)連接。

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");
}可以看到連接的獲取是通過(guò)工具類DataSourceUtils來(lái)操作完成的,這個(gè)是spring jdbc中所提供的工具類。
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");
//這里能看到連接是從事務(wù)同步器中拿的
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.
// 省略相關(guān)代碼。。。
return con;
}由于前面開(kāi)啟事務(wù)時(shí)已經(jīng)給當(dāng)前線程綁定了一個(gè)ConnectionHolder,這里就直接接能獲取到,這樣就實(shí)現(xiàn)了同一個(gè)線程中數(shù)據(jù)庫(kù)連接的共享。最后提交事務(wù)時(shí)是交給doCommit方法完成的。
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
// 最開(kāi)始創(chuàng)建一個(gè)新的事務(wù)時(shí)txObject中已經(jīng)綁定了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);
}
}
事務(wù)提交之后從數(shù)據(jù)源中拿到的Connection自動(dòng)提交要恢復(fù)為true。JdbcTransactionManager的操作是在父類DataSourceTransactionManager的doCleanupAfterCompletion方法中完成的。
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()) {
// 恢復(fù)為自動(dòng)提交
con.setAutoCommit(true);
}
DataSourceUtils.resetConnectionAfterTransaction(con,
txObject.getPreviousIsolationLevel(),
(txObject.isReadOnly() && !isDefaultReadOnly()));
}
catch (Throwable ex) {
logger.debug("Could not reset JDBC Connection after transaction", ex);
}
// 省略部分代碼。。。
}總結(jié)
為何mybatis的sqlSession在執(zhí)行同一個(gè)查詢sql語(yǔ)句時(shí)后續(xù)會(huì)從緩存中拿值。前面說(shuō)到sql語(yǔ)句的執(zhí)行會(huì)交給Executor,其查詢方法如下。
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.");
}
// 如果設(shè)置了強(qiáng)制刷新緩存,每次執(zhí)行查詢時(shí)都會(huì)清空一遍緩存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 如果從緩存中拿到了值就不從數(shù)據(jù)庫(kù)中查詢了
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接口時(shí)會(huì)默認(rèn)讓查詢語(yǔ)句的刷新緩存為都為false,這里其實(shí)就是mybatis的一級(jí)緩存,屬于會(huì)話級(jí)別。當(dāng)指定Options注解且其flushCache為true時(shí)會(huì)設(shè)置查詢語(yǔ)句要刷新緩存,如果是使用xml寫(xiě)sql語(yǔ)句,相應(yīng)的select標(biāo)簽上指定flushCache屬性為true。
到此這篇關(guān)于SpringBoot中如何手動(dòng)開(kāi)啟數(shù)據(jù)庫(kù)事務(wù)的文章就介紹到這了,更多相關(guān)SpringBoot手動(dòng)開(kāi)啟數(shù)據(jù)庫(kù)事務(wù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java數(shù)組復(fù)制的四種方法效率對(duì)比
這篇文章主要介紹了java數(shù)組復(fù)制的四種方法效率對(duì)比,文中有簡(jiǎn)單的代碼示例,以及效率的比較結(jié)果,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
Java運(yùn)用SWT插件編寫(xiě)桌面記事本應(yīng)用程序
這篇文章主要為大家介紹了一個(gè)Java項(xiàng)目實(shí)戰(zhàn),一步步教你實(shí)現(xiàn)記事本,步驟很詳細(xì),運(yùn)用SWT插件手把手編寫(xiě)記事本,感興趣的小伙伴們可以參考一下2016-01-01
mybatis-plus判斷參數(shù)是否為空并作為查詢條件方式
這篇文章主要介紹了mybatis-plus判斷參數(shù)是否為空并作為查詢條件方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2026-05-05
java 簡(jiǎn)單的計(jì)算器程序?qū)嵗a
這篇文章主要介紹了java 簡(jiǎn)單的計(jì)算器程序?qū)嵗a的相關(guān)資料,需要的朋友可以參考下2017-06-06
java本機(jī)內(nèi)存分配Native?memory?allocation?mmap失敗問(wèn)題解決
這篇文章主要介紹了java本機(jī)內(nèi)存分配Native?memory?allocation?mmap失敗問(wèn)題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
JDK從8升級(jí)到21的問(wèn)題集(附案例代碼)
JDK 8升級(jí)到JDK 21是一個(gè)重要的版本遷移,涉及語(yǔ)法、模塊化、API變更等多方面調(diào)整,這篇文章主要介紹了JDK從8升級(jí)到21問(wèn)題的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-09-09

