五分鐘教你手寫(xiě) SpringBoot 本地事務(wù)管理實(shí)現(xiàn)
白菜Java自習(xí)室 涵蓋核心知識(shí)
1. SpringBoot 事務(wù)
一直在用 SpringBoot 中的 @Transactional 來(lái)做事務(wù)管理,但是很少?zèng)]想過(guò) SpringBoot 是如何實(shí)現(xiàn)事務(wù)管理的,今天從源碼入手,看看 @Transactional 是如何實(shí)現(xiàn)事務(wù)的,最后我們結(jié)合源碼的理解,自己動(dòng)手寫(xiě)一個(gè)類似的注解來(lái)實(shí)現(xiàn)事務(wù)管理,幫助我們加深理解。
1.1. 事務(wù)的隔離級(jí)別
事務(wù)為什么需要隔離級(jí)別呢?這是因?yàn)樵诓l(fā)事務(wù)情況下,如果沒(méi)有隔離級(jí)別會(huì)導(dǎo)致如下問(wèn)題:
- 臟讀 (Dirty Read) :當(dāng)A事務(wù)對(duì)數(shù)據(jù)進(jìn)行修改,但是這種修改還沒(méi)有提交到數(shù)據(jù)庫(kù)中,B事務(wù)同時(shí)在訪問(wèn)這個(gè)數(shù)據(jù),由于沒(méi)有隔離,B獲取的數(shù)據(jù)有可能被A事務(wù)回滾,這就導(dǎo)致了數(shù)據(jù)不一致的問(wèn)題。
- 丟失修改 (Lost To Modify):當(dāng)A事務(wù)訪問(wèn)數(shù)據(jù)100,并且修改為100-1=99,同時(shí)B事務(wù)讀取數(shù)據(jù)也是100,修改數(shù)據(jù)100-1=99,最終兩個(gè)事務(wù)的修改結(jié)果為99,但是實(shí)際是98。事務(wù)A修改的數(shù)據(jù)被丟失了。
- 不可重復(fù)讀 (Unrepeatable Read):指A事務(wù)在讀取數(shù)據(jù)X=100的時(shí)候,B事務(wù)把數(shù)據(jù)X=100修改為X=200,這個(gè)時(shí)候A事務(wù)第二次讀取數(shù)據(jù)X的時(shí)候,發(fā)現(xiàn)X=200了,導(dǎo)致了在整個(gè)A事務(wù)期間,兩次讀取數(shù)據(jù)X不一致了,這就是不可重復(fù)讀。
- 幻讀 (Phantom Read):幻讀和不可重復(fù)讀類似?;米x表現(xiàn)在,當(dāng)A事務(wù)讀取表數(shù)據(jù)時(shí)候,只有3條數(shù)據(jù),這個(gè)時(shí)候B事務(wù)插入了2條數(shù)據(jù),當(dāng)A事務(wù)再次讀取的時(shí)候,發(fā)現(xiàn)有5條記錄了,平白無(wú)故多了2條記錄,就像幻覺(jué)一樣。
不可重復(fù)讀 VS 幻讀
不可重復(fù)讀的重點(diǎn)是修改 :同樣的條件 , 你讀取過(guò)的數(shù)據(jù) , 再次讀取出來(lái)發(fā)現(xiàn)值不一樣了,重點(diǎn)在更新操作。
幻讀的重點(diǎn)在于新增或者刪除:同樣的條件 , 第 1 次和第 2 次讀出來(lái)的記錄數(shù)不一樣,重點(diǎn)在增刪操作。
所以,為了避免上述的問(wèn)題,事務(wù)中就有了隔離級(jí)別的概念,在Spring中定義了五種表示隔離級(jí)別的常量 TransactionDefinition:
- ISOLATION_DEFAULT:數(shù)據(jù)庫(kù)默認(rèn)的隔離級(jí)別,MySQL默認(rèn)采用的 REPEATABLE_READ 隔離級(jí)別。
- ISOLATION_READ_UNCOMMITTED:最低的隔離級(jí)別,允許讀取未提交的數(shù)據(jù)變更,可能會(huì)導(dǎo)致臟讀、幻讀或不可重復(fù)讀。
- ISOLATION_READ_COMMITTED:允許讀取并發(fā)事務(wù)已經(jīng)提交的數(shù)據(jù),可以阻止臟讀,但是幻讀或不可重復(fù)讀仍有可能發(fā)生。
- ISOLATION_REPEATABLE_READ:對(duì)同一字段的多次讀取結(jié)果都是一致的,除非數(shù)據(jù)是被本身事務(wù)自己所修改,可以阻止臟讀和不可重復(fù)讀,但幻讀仍有可能發(fā)生。MySQL中通過(guò)MVCC解決了該隔離級(jí)別下出現(xiàn)幻讀的可能。
- ISOLATION_SERIALIZABLE:串行化隔離級(jí)別,該級(jí)別可以防止臟讀、不可重復(fù)讀以及幻讀,但是串行化會(huì)影響性能。
1.2. Spring中事務(wù)的傳播機(jī)制
為什么Spring中要搞一套事務(wù)的傳播機(jī)制呢?這是Spring給我們提供的事務(wù)增強(qiáng)工具,主要是解決方法之間調(diào)用,事務(wù)如何處理的問(wèn)題。比如有方法A、方法B和方法C,在A中調(diào)用了方法B和方法C。偽代碼如下:
MethodA() {
MethodB();
MethodC();
}
假設(shè)三個(gè)方法中都開(kāi)啟了自己的事務(wù),那么他們之間是什么關(guān)系呢?MethodA的回滾會(huì)影響MethodB和MethodC嗎?Spring中的事務(wù)傳播機(jī)制就是解決這個(gè)問(wèn)題的。
Spring中定義了七種事務(wù)傳播行為:
- PROPAGATION_REQUIRED: 如果存在一個(gè)事務(wù),則支持當(dāng)前事務(wù)。如果沒(méi)有事務(wù)則開(kāi)啟一個(gè)新的事務(wù)。
- PROPAGATION_SUPPORTS: 如果存在一個(gè)事務(wù),支持當(dāng)前事務(wù)。如果沒(méi)有事務(wù),則非事務(wù)的執(zhí)行。但是對(duì)于事務(wù)同步的事務(wù)管理器,PROPAGATION_SUPPORTS與不使用事務(wù)有少許不同。
- PROPAGATION_MANDATORY: 如果已經(jīng)存在一個(gè)事務(wù),支持當(dāng)前事務(wù)。如果沒(méi)有一個(gè)活動(dòng)的事務(wù),則拋出異常。
- PROPAGATION_REQUIRES_NEW: 總是開(kāi)啟一個(gè)新的事務(wù)。如果一個(gè)事務(wù)已經(jīng)存在,則將這個(gè)存在的事務(wù)掛起。
- PROPAGATION_NOT_SUPPORTED: 總是非事務(wù)地執(zhí)行,并掛起任何存在的事務(wù)。
- PROPAGATION_NEVER: 總是非事務(wù)地執(zhí)行,如果存在一個(gè)活動(dòng)事務(wù),則拋出異常。
- PROPAGATION_NESTED: 如果一個(gè)活動(dòng)的事務(wù)存在,則運(yùn)行在一個(gè)嵌套的事務(wù)中。 如果沒(méi)有活動(dòng)事務(wù), 則按 TransactionDefinition.PROPAGATION_REQUIRED 屬性執(zhí)行。
1.3. Spring中事務(wù)如何實(shí)現(xiàn)異常回滾的
回顧完了事務(wù)的相關(guān)知識(shí),接下來(lái)我們正式來(lái)研究下 Spring Boot 中如何通過(guò) @Transactional 來(lái)管理事務(wù)的,我們重點(diǎn)看看它是如何實(shí)現(xiàn)回滾的。
在 Spring 中 TransactionInterceptor 和 PlatformTransactionManager 這兩個(gè)類是整個(gè)事務(wù)模塊的核心,我們重點(diǎn)研究下這兩個(gè)類的源碼。
- TransactionInterceptor 負(fù)責(zé)攔截方法執(zhí)行,進(jìn)行判斷是否需要提交或者回滾事務(wù)。
- PlatformTransactionManager 是 Spring 中的事務(wù)管理接口,真正定義了事務(wù)如何回滾和提交。
TransactionInterceptor 類中的代碼有很多,我簡(jiǎn)化一下邏輯,方便說(shuō)明:
// 以下代碼省略部分內(nèi)容
public Object invoke(MethodInvocation invocation) throws Throwable {
// 獲取事務(wù)調(diào)用的目標(biāo)方法
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// 執(zhí)行帶事務(wù)調(diào)用
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
invokeWithinTransaction 簡(jiǎn)化邏輯如下:
// 以下代碼省略部分內(nèi)容
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
Object retVal;
try {
// 調(diào)用真正的方法體
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 如果出現(xiàn)異常,執(zhí)行事務(wù)異常處理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 最后做一下清理工作,主要是緩存和狀態(tài)等
cleanupTransactionInfo(txInfo);
}
// 如果沒(méi)有異常,直接提交事務(wù)
commitTransactionAfterReturning(txInfo);
return retVal;
}
事務(wù)出現(xiàn)異?;貪L的邏輯 completeTransactionAfterThrowing 如下:
// 以下代碼省略部分內(nèi)容
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
// 判斷是否需要回滾,判斷的邏輯就是看有沒(méi)有聲明事務(wù)屬性,同時(shí)判斷是不是在目前的這個(gè)異常中執(zhí)行回滾
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
// 執(zhí)行回滾
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
else {
// 否則不需要回滾,直接提交即可
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}
上面的代碼已經(jīng)把 Spring 的事務(wù)的基本原理說(shuō)清楚了,如何進(jìn)行判斷執(zhí)行事務(wù),如何回滾。下面到了真正執(zhí)行回滾邏輯的代碼中 PlatformTransactionManager 接口的子類,我們以 JDBC 的事務(wù)為例,DataSourceTransactionManager 就是 jdbc 的事務(wù)管理類。跟蹤上面的代碼rollback(txInfo.getTransactionStatus()) 可以發(fā)現(xiàn)最終執(zhí)行的代碼如下:
@Override
protected void doRollback(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
}
try {
// 調(diào)用jdbc的 rollback進(jìn)行回滾事務(wù)
con.rollback();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
}
}
這里小結(jié)下 Spring 中事務(wù)的實(shí)現(xiàn)思路,Spring 主要依靠 TransactionInterceptor 來(lái)攔截執(zhí)行方法體,判斷是否開(kāi)啟事務(wù),然后執(zhí)行事務(wù)方法體,方法體中 catch 住異常,接著判斷是否需要回滾,如果需要回滾就委托真正的 TransactionManager 比如 JDBC 中的 DataSourceTransactionManager 來(lái)執(zhí)行回滾邏輯。提交事務(wù)也是同樣的道理。
這里用個(gè)流程圖展示下思路:

2. 手寫(xiě)注解實(shí)現(xiàn)事務(wù)回滾
我們弄清楚了 Spring 的事務(wù)執(zhí)行流程,那我們可以模仿著自己寫(xiě)一個(gè)注解,實(shí)現(xiàn)遇到指定異常就回滾的功能。這里持久層就以最簡(jiǎn)單的 JDBC 為例。我們先梳理下需求,首先注解我們可以基于 Spring 的 AOP 來(lái)實(shí)現(xiàn),接著既然是 JDBC,那么我們需要一個(gè)類來(lái)幫我們管理連接,用來(lái)判斷異常是否回滾或者提交。
2.1. Maven 加入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
2.2. 新建一個(gè)注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
// 指定異?;貪L
Class<? extends Throwable>[] rollbackFor() default {};
}
2.3. 新建連接管理器
該類幫助我們管理連接,該類的核心功能是把取出的連接對(duì)象綁定到線程上,方便在 AOP 處理中取出,進(jìn)行提交或者回滾操作。
@Component
public class DataSourceConnectHolder {
@Autowired
private DataSource dataSource;
/**
* 線程綁定對(duì)象
*/
ThreadLocal<Connection> resources = new NamedThreadLocal<>("Transactional resources");
public Connection getConnection() {
Connection con = resources.get();
if (con != null) {
return con;
}
try {
con = dataSource.getConnection();
// 為了體現(xiàn)事務(wù),全部設(shè)置為手動(dòng)提交事務(wù)
con.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
resources.set(con);
return con;
}
public void cleanHolder() {
Connection con = resources.get();
if (con != null) {
try {
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
resources.remove();
}
}
2.4. 新建一個(gè)切面
這部分是事務(wù)處理的核心,先獲取注解上的異常類,然后捕獲住執(zhí)行的異常,判斷異常是不是注解上的異常或者其子類,如果是就回滾,否則就提交。
@Aspect
@Component
public class MyTransactionAopHandler {
@Autowired
private DataSourceConnectHolder connectHolder;
Class<? extends Throwable>[] es;
// 攔截所有MyTransaction注解的方法
@org.aspectj.lang.annotation.Pointcut("@annotation(你的包路徑.MyTransaction)")
public void Transaction() {
}
@Around("Transaction()")
public Object TransactionProceed(ProceedingJoinPoint proceed) throws Throwable {
Object result = null;
Signature signature = proceed.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method == null) {
return result;
}
MyTransaction transaction = method.getAnnotation(MyTransaction.class);
if (transaction != null) {
es = transaction.rollbackFor();
}
try {
result = proceed.proceed();
} catch (Throwable throwable) {
// 異常處理
completeTransactionAfterThrowing(throwable);
throw throwable;
}
// 直接提交
doCommit();
return result;
}
/**
* 執(zhí)行回滾,最后關(guān)閉連接和清理線程綁定
*/
private void doRollBack() {
try {
connectHolder.getConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
} finally {
connectHolder.cleanHolder();
}
}
/**
* 執(zhí)行提交,最后關(guān)閉連接和清理線程綁定
*/
private void doCommit() {
try {
connectHolder.getConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
} finally {
connectHolder.cleanHolder();
}
}
/**
* 異常處理,捕獲的異常是目標(biāo)異?;蛘咂渥宇?,就進(jìn)行回滾,否則就提交事務(wù)。
*/
private void completeTransactionAfterThrowing(Throwable throwable) {
if (es != null && es.length > 0) {
for (Class<? extends Throwable> e : es) {
if (e.isAssignableFrom(throwable.getClass())) {
doRollBack();
}
}
}
doCommit();
}
}
2.4. 編寫(xiě)一個(gè) Service
saveTest 方法調(diào)用了2個(gè)插入語(yǔ)句,同時(shí)聲明了 @MyTransaction 事務(wù)注解,遇到 Exception 就進(jìn)行回滾。
@Service
public class MyTransactionTest {
@Autowired
private DataSourceConnectHolder holder;
// 一個(gè)事務(wù)中執(zhí)行兩個(gè)sql插入
@MyTransaction(rollbackFor = NullPointerException.class)
public void saveTest(int id) {
save(id, "白菜Java自習(xí)室");
save(id + 10, "白菜Java自習(xí)室");
throw new RuntimeException();
}
// 執(zhí)行sql
private void save(int id, String value) {
String sql = "insert into test values(?,?)";
Connection connection = holder.getConnection();
PreparedStatement stmt = null;
try {
stmt = connection.prepareStatement(sql);
stmt.setInt(1, id);
stmt.setString(2, value);
stmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
我們自己通過(guò) JDBC 結(jié)合 Spring 的 AOP 自己寫(xiě)了個(gè) @MyTransactional 的注解,實(shí)現(xiàn)了遇到指定異常回滾的功能。
到此這篇關(guān)于五分鐘教你手寫(xiě) SpringBoot 本地事務(wù)管理實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot 本地事務(wù)管理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring聲明式事務(wù)注解之@EnableTransactionManagement解析
- spring學(xué)習(xí)JdbcTemplate數(shù)據(jù)庫(kù)事務(wù)管理
- Spring框架JdbcTemplate數(shù)據(jù)庫(kù)事務(wù)管理完全注解方式
- 解析spring事務(wù)管理@Transactional為什么要添加rollbackFor=Exception.class
- SpringBoot2整合JTA組件實(shí)現(xiàn)多數(shù)據(jù)源事務(wù)管理
- Spring事務(wù)管理原理及方法詳解
- 啟用Spring事務(wù)管理@EnableTransactionManagement示例解析
相關(guān)文章
ThreadLocal簡(jiǎn)介_(kāi)動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了ThreadLocal簡(jiǎn)介的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
使用Postman傳遞arraylist數(shù)據(jù)給springboot方式
這篇文章主要介紹了使用Postman傳遞arraylist數(shù)據(jù)給springboot方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
關(guān)于Nacos和Eureka的區(qū)別及說(shuō)明
這篇文章主要介紹了關(guān)于Nacos和Eureka的區(qū)別及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
java實(shí)現(xiàn)新浪微博Oauth接口發(fā)送圖片和文字的方法
這篇文章主要介紹了java實(shí)現(xiàn)新浪微博Oauth接口發(fā)送圖片和文字的方法,涉及java調(diào)用新浪微博Oauth接口的使用技巧,具有一定參考接借鑒價(jià)值,需要的朋友可以參考下2015-07-07
Maven導(dǎo)入依賴時(shí)報(bào)錯(cuò)如何解決
這篇文章主要介紹了Maven導(dǎo)入依賴時(shí)報(bào)錯(cuò)如何解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12
Mybatis?TypeHandler接口及繼承關(guān)系示例解析
這篇文章主要為大家介紹了Mybatis?TypeHandler接口及繼承關(guān)系示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
利用SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的兩種方式總結(jié)
關(guān)于動(dòng)態(tài)數(shù)據(jù)源的切換的方案有很多,核心只有兩種,一種是構(gòu)建多套環(huán)境,另一種是基于spring原生的AbstractRoutingDataSource切換,這篇文章主要給大家介紹了關(guān)于利用SpringBoot實(shí)現(xiàn)多數(shù)據(jù)源的兩種方式,需要的朋友可以參考下2021-10-10

