概述
一直在用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í)對(duì)事務(wù)有基本的了解和使用独榴。
事務(wù)的相關(guān)知識(shí)
開始看源碼之前僧叉,我們先回顧下事務(wù)的相關(guān)知識(shí)。
1棺榔、事務(wù)的隔離級(jí)別
事務(wù)為什么需要隔離級(jí)別呢瓶堕?這是因?yàn)樵诓l(fā)事務(wù)情況下,如果沒有隔離級(jí)別會(huì)導(dǎo)致如下問題:
臟讀(Dirty Read) :當(dāng)A事務(wù)對(duì)數(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ù)讀類似】缢悖幻讀表現(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ù)中就有了隔離級(jí)別的概念,在Spring中定義了五種表示隔離級(jí)別的常量:
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ù)傳播行為:
如何實(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)異常回滾的邏輯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);
}
}
小結(jié)
這里小結(jié)下Spring 中事務(wù)的實(shí)現(xiàn)思路,Spring 主要依靠 TransactionInterceptor 來攔截執(zhí)行方法體痹扇,判斷是否開啟事務(wù)铛漓,然后執(zhí)行事務(wù)方法體,方法體中catch住異常,接著判斷是否需要回滾鲫构,如果需要回滾就委托真正的TransactionManager 比如JDBC中的DataSourceTransactionManager
來執(zhí)行回滾邏輯浓恶。提交事務(wù)也是同樣的道理。
這里用個(gè)流程圖展示下思路:
手寫一個(gè)注解實(shí)現(xiàn)事務(wù)回滾
我們弄清楚了Spring的事務(wù)執(zhí)行流程结笨,那我們可以模仿著自己寫一個(gè)注解包晰,實(shí)現(xiàn)遇到指定異常就回滾的功能。這里持久層就以最簡單的JDBC為例炕吸。
我們先梳理下需求伐憾,首先注解我們可以基于Spring 的AOP來實(shí)現(xiàn),接著既然是JDBC,那么我們需要一個(gè)類來幫我們管理連接赫模,用來判斷異常是否回滾或者提交树肃。梳理完就開干吧。
1嘴瓤、首先加入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、新增一個(gè)注解
/**
* @description:
* @author: luozhou
* @create: 2020-03-29 17:05
**/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MyTransaction {
//指定異忱蚋疲回滾
Class<? extends Throwable>[] rollbackFor() default {};
}
3廓脆、新增連接管理器
該類幫助我們管理連接,該類的核心功能是把取出的連接對(duì)象綁定到線程上磁玉,方便在AOP處理中取出停忿,進(jìn)行提交或者回滾操作。
/**
* @description:
* @author: luozhou
* @create: 2020-03-29 21:14
**/
@Component
public class DataSourceConnectHolder {
@Autowired
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();
}
}
4席赂、新增一個(gè)切面
這部分是事務(wù)處理的核心,先獲取注解上的異常類时迫,然后捕獲住執(zhí)行的異常颅停,判斷異常是不是注解上的異常或者其子類掠拳,如果是就回滾癞揉,否則就提交。
/**
* @description:
* @author: luozhou
* @create: 2020-03-29 17:08
**/
@Aspect
@Component
public 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)異常或者其子類芥牌,就進(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();
}
}
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;
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
**/
@Service
public 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();
}
}
}
2)單元測試
@SpringBootTest
@RunWith(SpringRunner.class)
class SpringTransactionApplicationTests {
@Autowired
private TestService service;
@Test
void contextLoads() throws SQLException {
service.saveTest(1);
}
}
上圖代碼聲明了事務(wù)對(duì)NullPointerException
異常進(jìn)行回滾,運(yùn)行中遇到了ArithmeticException
異常控汉,所以是不會(huì)回滾的笔诵,我們?cè)谟疫叺臄?shù)據(jù)庫中刷新發(fā)現(xiàn)數(shù)據(jù)正常插入成功了,說明并沒有回滾姑子。
我們把回滾的異常類改為ArithmeticException,把原數(shù)據(jù)清空再執(zhí)行一次乎婿,出現(xiàn)了ArithmeticException異常,這個(gè)時(shí)候查看數(shù)據(jù)庫是沒有記錄新增成功了街佑,這說明事物進(jìn)行回滾了谢翎,表明我們的注解起作用了。
總結(jié)
本文最開始回顧了事務(wù)的相關(guān)知識(shí)沐旨,并發(fā)事務(wù)會(huì)導(dǎo)致臟讀森逮、丟失修改、不可重復(fù)讀磁携、幻讀褒侧,為了解決這些問題,數(shù)據(jù)庫中就引入了事務(wù)的隔離級(jí)別谊迄,隔離級(jí)別包括:讀未提交闷供、讀提交、可重復(fù)讀和串行化统诺。
Spring中增強(qiáng)了事務(wù)的概念歪脏,為了解決方法A、方法B和方法C之間的事務(wù)關(guān)系粮呢,引入了事務(wù)傳播機(jī)制的概念唾糯。
Spring中的@Transactiona
l注解的事務(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)了遇到指定異称厦耄回滾的功能。
作者:木木匠
原文鏈接:https://juejin.im/post/5e7ef0bae51d4546f16bb3fb
文源網(wǎng)絡(luò)嵌溢,僅供學(xué)習(xí)之用眯牧,如有侵權(quán)請(qǐng)聯(lián)系刪除。
我將面試題和答案都整理成了PDF文檔赖草,還有一套學(xué)習(xí)資料学少,涵蓋Java虛擬機(jī)、spring框架秧骑、Java線程版确、數(shù)據(jù)結(jié)構(gòu)、設(shè)計(jì)模式等等乎折,但不僅限于此绒疗。
關(guān)注公眾號(hào)【java圈子】獲取資料,還有優(yōu)質(zhì)文章每日送達(dá)骂澄。