javascript
springboot 事务_Spring Boot中的事务是如何实现的?懂吗?
- 一個(gè)SpringBoot問題就干趴下了?我卻憑著這份PDF文檔吊打面試官.
- 金三銀四第一天,啃透這些SpringBoot知識(shí)點(diǎn),還怕干不贏面試官?
- Spring全家桶筆記:Spring+Spring Boot+Spring Cloud+Spring MVC
1. 概述
一直在用SpringBoot中的@Transactional來做事務(wù)管理,但是很少?zèng)]想過SpringBoot是如何實(shí)現(xiàn)事務(wù)管理的,今天從源碼入手,看看@Transactional是如何實(shí)現(xiàn)事務(wù)的,最后我們結(jié)合源碼的理解,自己動(dòng)手寫一個(gè)類似的注解來實(shí)現(xiàn)事務(wù)管理,幫助我們加深理解。
閱讀說明:本文假設(shè)你具備Java基礎(chǔ),同時(shí)對事務(wù)有基本的了解和使用。
2. 事務(wù)的相關(guān)知識(shí)
開始看源碼之前,我們先回顧下事務(wù)的相關(guān)知識(shí)。
2.1 事務(wù)的隔離級別
事務(wù)為什么需要隔離級別呢?這是因?yàn)樵诓l(fā)事務(wù)情況下,如果沒有隔離級別會(huì)導(dǎo)致如下問題:
- 臟讀(Dirty Read) :當(dāng)A事務(wù)對數(shù)據(jù)進(jìn)行修改,但是這種修改還沒有提交到數(shù)據(jù)庫中,B事務(wù)同時(shí)在訪問這個(gè)數(shù)據(jù),由于沒有隔離,B獲取的數(shù)據(jù)有可能被A事務(wù)回滾,這就導(dǎo)致了數(shù)據(jù)不一致的問題。
- 丟失修改(Lost To Modify): 當(dāng)A事務(wù)訪問數(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條記錄了,平白無故多了2條記錄,就像幻覺一樣。
不可重復(fù)讀 VS 幻讀
不可重復(fù)讀的重點(diǎn)是修改 : 同樣的條件 , 你讀取過的數(shù)據(jù) , 再次讀取出來發(fā)現(xiàn)值不一樣了,重點(diǎn)在更新操作。 幻讀的重點(diǎn)在于新增或者刪除:同樣的條件 , 第 1 次和第 2 次讀出來的記錄數(shù)不一樣,重點(diǎn)在增刪操作。
所以,為了避免上述的問題,事務(wù)中就有了隔離級別的概念,在Spring中定義了五種表示隔離級別的常量:
常量說明TransactionDefinition.ISOLATION_DEFAULT數(shù)據(jù)庫默認(rèn)的隔離級別,MySQL默認(rèn)采用的 REPEATABLE_READ隔離級別TransactionDefinition.ISOLATION_READ_UNCOMMITTED最低的隔離級別,允許讀取未提交的數(shù)據(jù)變更,可能會(huì)導(dǎo)致臟讀、幻讀或不可重復(fù)讀。TransactionDefinition.ISOLATION_READ_COMMITTED允許讀取并發(fā)事務(wù)已經(jīng)提交的數(shù)據(jù),可以阻止臟讀,但是幻讀或不可重復(fù)讀仍有可能發(fā)生。TransactionDefinition.ISOLATION_REPEATABLE_READ對同一字段的多次讀取結(jié)果都是一致的,除非數(shù)據(jù)是被本身事務(wù)自己所修改,可以阻止臟讀和不可重復(fù)讀,但幻讀仍有可能發(fā)生。MySQL中通過MVCC解決了該隔離級別下出現(xiàn)幻讀的可能。TransactionDefinition.ISOLATION_SERIALIZABLE串行化隔離級別,該級別可以防止臟讀、不可重復(fù)讀以及幻讀,但是串行化會(huì)影響性能。
2.2 Spring中事務(wù)的傳播機(jī)制
為什么Spring中要搞一套事務(wù)的傳播機(jī)制呢?這是Spring給我們提供的事務(wù)增強(qiáng)工具,主要是解決方法之間調(diào)用,事務(wù)如何處理的問題。比如有方法A、方法B和方法C,在A中調(diào)用了方法B和方法C。偽代碼如下:
MethodA{ MethodB; MethodC;}MethodB{}MethodC{}假設(shè)三個(gè)方法中都開啟了自己的事務(wù),那么他們之間是什么關(guān)系呢?MethodA的回滾會(huì)影響MethodB和MethodC嗎?Spring中的事務(wù)傳播機(jī)制就是解決這個(gè)問題的。
Spring中定義了七種事務(wù)傳播行為:
3. 如何實(shí)現(xiàn)異常回滾的
回顧完了事務(wù)的相關(guān)知識(shí),接下來我們正式來研究下Spring Boot中如何通過@Transactional來管理事務(wù)的,我們重點(diǎn)看看它是如何實(shí)現(xiàn)回滾的。
在Spring中TransactionInterceptor和PlatformTransactionManager這兩個(gè)類是整個(gè)事務(wù)模塊的核心,TransactionInterceptor負(fù)責(zé)攔截方法執(zhí)行,進(jìn)行判斷是否需要提交或者回滾事務(wù)。PlatformTransactionManager是Spring 中的事務(wù)管理接口,真正定義了事務(wù)如何回滾和提交。我們重點(diǎn)研究下這兩個(gè)類的源碼。
TransactionInterceptor類中的代碼有很多,我簡化一下邏輯,方便說明:
//以下代碼省略部分內(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 簡化邏輯如下:
//TransactionAspectSupport.class //省略了部分代碼 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); } //如果沒有異常,直接提交事務(wù)。 commitTransactionAfterReturning(txInfo); return retVal; }事務(wù)出現(xiàn)異?;貪L的邏輯completeTransactionAfterThrowing如下:
//省略部分代碼protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { //判斷是否需要回滾,判斷的邏輯就是看有沒有聲明事務(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ù)的基本原理說清楚了,如何進(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); } }3.1 小結(jié)
這里小結(jié)下Spring 中事務(wù)的實(shí)現(xiàn)思路,Spring 主要依靠 TransactionInterceptor 來攔截執(zhí)行方法體,判斷是否開啟事務(wù),然后執(zhí)行事務(wù)方法體,方法體中catch住異常,接著判斷是否需要回滾,如果需要回滾就委托真正的TransactionManager 比如JDBC中的DataSourceTransactionManager來執(zhí)行回滾邏輯。提交事務(wù)也是同樣的道理。
這里用個(gè)流程圖展示下思路:
4. 手寫一個(gè)注解實(shí)現(xiàn)事務(wù)回滾
我們弄清楚了Spring的事務(wù)執(zhí)行流程,那我們可以模仿著自己寫一個(gè)注解,實(shí)現(xiàn)遇到指定異常就回滾的功能。這里持久層就以最簡單的JDBC為例。我們先梳理下需求,首先注解我們可以基于Spring 的AOP來實(shí)現(xiàn),接著既然是JDBC,那么我們需要一個(gè)類來幫我們管理連接,用來判斷異常是否回滾或者提交。梳理完就開干吧。
4.1 首先加入依賴
org.springframework.boot spring-boot-starter-jdbc org.springframework.boot spring-boot-starter-aop4.2 新增一個(gè)注解
/** * @description: * @author: luozhou * @create: 2020-03-29 17:05 **/@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface MyTransaction { //指定異?;貪L Class extends Throwable>[] rollbackFor() default {};}4.3 新增連接管理器
該類幫助我們管理連接,該類的核心功能是把取出的連接對象綁定到線程上,方便在AOP處理中取出,進(jìn)行提交或者回滾操作。
/** * @description: * @author: luozhou * @create: 2020-03-29 21:14 **/@Componentpublic class DataSourceConnectHolder { @Autowired DataSource dataSource; /** * 線程綁定對象 */ ThreadLocal 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(); }}4.4 新增一個(gè)切面
這部分是事務(wù)處理的核心,先獲取注解上的異常類,然后捕獲住執(zhí)行的異常,判斷異常是不是注解上的異?;蛘咂渥宇?#xff0c;如果是就回滾,否則就提交。
/** * @description: * @author: luozhou * @create: 2020-03-29 17:08 **/@Aspect@Componentpublic class MyTransactionAopHandler { @Autowired DataSourceConnectHolder connectHolder; Class extends Throwable>[] es; //攔截所有MyTransaction注解的方法 @org.aspectj.lang.annotation.Pointcut("@annotation(luozhou.top.annotion.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)異?;蛘咂渥宇?#xff0c;就進(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(); }}4.5 測試驗(yàn)證
創(chuàng)建一個(gè)tb_test表,表結(jié)構(gòu)如下:
SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for tb_test-- ----------------------------DROP TABLE IF EXISTS `tb_test`;CREATE TABLE `tb_test` ( `id` int(11) NOT NULL, `email` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=latin1;SET FOREIGN_KEY_CHECKS = 1;4.5.1 編寫一個(gè)Service
saveTest方法調(diào)用了2個(gè)插入語句,同時(shí)聲明了@MyTransaction事務(wù)注解,遇到NullPointerException就進(jìn)行回滾,最后我們執(zhí)行了除以0操作,會(huì)拋出ArithmeticException。我們用單元測試看看數(shù)據(jù)是否會(huì)回滾。
/** * @description: * @author: luozhou kinglaw1204@gmail.com * @create: 2020-03-29 22:05 **/@Servicepublic class MyTransactionTest implements TestService { @Autowired DataSourceConnectHolder holder; //一個(gè)事務(wù)中執(zhí)行兩個(gè)sql插入 @MyTransaction(rollbackFor = NullPointerException.class) @Override public void saveTest(int id) { saveWitharamters(id, "luozhou@gmail.com"); saveWitharamters(id + 10, "luozhou@gmail.com"); int aa = id / 0; } //執(zhí)行sql private void saveWitharamters(int id, String email) { String sql = "insert into tb_test values(?,?)"; Connection connection = holder.getConnection(); PreparedStatement stmt = null; try { stmt = connection.prepareStatement(sql); stmt.setInt(1, id); stmt.setString(2, email); stmt.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }}4.5.2 單元測試
@SpringBootTest@RunWith(SpringRunner.class)class SpringTransactionApplicationTests { @Autowired private TestService service; @Test void contextLoads() throws SQLException { service.saveTest(1); }}上圖代碼聲明了事務(wù)對NullPointerException異常進(jìn)行回滾,運(yùn)行中遇到了ArithmeticException異常,所以是不會(huì)回滾的,我們在右邊的數(shù)據(jù)庫中刷新發(fā)現(xiàn)數(shù)據(jù)正常插入成功了,說明并沒有回滾。
我們把回滾的異常類改為ArithmeticException,把原數(shù)據(jù)清空再執(zhí)行一次,出現(xiàn)了ArithmeticException異常,這個(gè)時(shí)候查看數(shù)據(jù)庫是沒有記錄新增成功了,這說明事物進(jìn)行回滾了,表明我們的注解起作用了。
5. 總結(jié)
本文最開始回顧了事務(wù)的相關(guān)知識(shí),并發(fā)事務(wù)會(huì)導(dǎo)致臟讀、丟失修改、不可重復(fù)讀、幻讀,為了解決這些問題,數(shù)據(jù)庫中就引入了事務(wù)的隔離級別,隔離級別包括:讀未提交、讀提交、可重復(fù)讀和串行化。
Spring中增強(qiáng)了事務(wù)的概念,為了解決方法A、方法B和方法C之間的事務(wù)關(guān)系,引入了事務(wù)傳播機(jī)制的概念。
Spring中的@Transactional注解的事務(wù)實(shí)現(xiàn)主要通過TransactionInterceptor攔截器來進(jìn)行實(shí)現(xiàn)的,攔截目標(biāo)方法,然后判斷異常是不是目標(biāo)異常,如果是目標(biāo)異常就行進(jìn)行回滾,否則就進(jìn)行事務(wù)提交。
最后我們自己通過JDBC結(jié)合Spring的AOP自己寫了個(gè)@MyTransactional的注解,實(shí)現(xiàn)了遇到指定異?;貪L的功能。
作者:木木匠
原文鏈接鏈接:https://juejin.im/post/5e7ef0bae51d4546f16bb3fb
總結(jié)
以上是生活随笔為你收集整理的springboot 事务_Spring Boot中的事务是如何实现的?懂吗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 吴恩达 ML作业提交:Grader se
- 下一篇: Python报错:The truth v