目標:事務失效引發的災難
如下圖(張三--->李四轉賬)

tips
下訂單-------訂單支付-----減庫存(失敗)
超賣現象
代碼回憶:
//實作類
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Resource
private LogService logService;
@Override
@Transactional
// @Transactional(rollbackFor = Exception.class)
public void insert() throws Exception {
method1_Test();
}
//模擬轉賬@Transactional
private void method1_Test() throws Exception {
System.out.println(">>>>>>>>>>>進入到業務方法");
User user = new User();
user.setName("張三");
userMapper.insertUser(user);//張三扣減500元
addPayment();//模擬李四增加500元(檢查例外)
}
//FileNotFoundException extends IOException
private void addPayment() throws FileNotFoundException {
FileInputStream in = new FileInputStream("a.txt");//模擬檢查例外
}
}
......略
如果說你從從事務方法中拋出的是檢查例外(io、sql),那么這個時候,Spring將不能進行事務回滾,
是不是很恐怖呢??
所以說,阿里規定
1、讓檢查例外也回滾:你就需要在整個方法前加上@Transactional(rollbackFor=Exception.class)
2、讓非檢查例外不回滾:
需要加入@Transactional(notRollbackFor=RunTimeException.class)
3、不需要事務管理(or 日志丟失)
需要加入@Transactional(propagation=Propagation.NOT_SUPPORTED)
課程目標總結
1、解決事務失效:通過原始碼學習如何讓檢查例外也回滾(or 運行例外不回滾);從原始碼角度深入底層原理
2、解決無需事務控制;查詢 or 日志記錄;通過傳播屬性如何控制;底層是如何實作的
3、正常的事務執行流程在原始碼中是如何實作的
1.1 Spring事務總體介紹
在Spring中,事務有兩種實作方式:
-
編程式事務管理: 編程式事務管理使用TransactionTemplate可實作更細粒度的事務控制,
-
申明式事務管理: 基于Spring AOP實作,
其本質是對方法前后進行攔截,然后在目標方法開始之前創建或者加入一個事務,在執行完目標方法之后根據執行情況提交或者回滾事務,
宣告式事務好處:
申明式事務管理不需要入侵代碼,通過@Transactional就可以進行事務操作,更快捷而且簡單,且大部分業務都可以滿足,推薦使用,
管是編程式事務還是申明式事務,最終呼叫的底層核心代碼是一致的
1.1.1 編程式事務實作方式
編程式事務,Spring已經給我們提供好了模板類TransactionTemplate,可以很方便的使用,如下圖

TransactionTemplate全路徑名是:org.springframework.transaction.support.TransactionTemplate,這是spring對事務的模板類
實作的介面
用來執行事務的回呼方法,
public interface TransactionOperations {
@Nullable
<T> T execute(TransactionCallback<T> action) throws TransactionException;
}
InitializingBean這個是典型的spring bean初始化流程中 ,用來在bean屬性加載完畢時執行的方法,
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
TransactionTemplate的2個介面的impl方法做了什么?
afterPropertiesSet如下
// 只是校驗了事務管理器不為空
@Override
public void afterPropertiesSet() {
if (this.transactionManager == null) {
throw new IllegalArgumentException("Property 'transactionManager' is required");
}
}
execute方法如下
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
//TODO 創建事務 (與宣告事務呼叫同一個方法)
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
// 2.執行業務邏輯,這里就是用戶自定義的業務代碼,如果是沒有回傳值的,就是doInTransactionWithoutResult(),
result = action.doInTransaction(status);
} catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
// 應用運行時例外/錯誤例外 -> 回滾,呼叫AbstractPlatformTransactionManager的rollback(),事務提交回滾
//TODO 回滾((與宣告事務呼叫同一個方法)
rollbackOnException(status, ex);
throw ex;
} catch (Throwable ex) {
// 未知例外 -> 回滾,呼叫AbstractPlatformTransactionManager的rollback(),事務提交回滾
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
// TODO 事務提交 (與宣告事務呼叫同一個方法)
this.transactionManager.commit(status);
return result;
}
}
總結
事務模板TransactionTemplateI里面的execute方法【創建事務】【提交事務】【回滾事務】和宣告式事務呼叫的都是同一個底層方法
1.1.2 宣告式事務實作方式
宣告式事務的用法
@Transactional注解可以加在類或方法上
1、類:在類上時是對該類的所有public方法開啟事務,
2、方法:加在方法上時也是只對public方法起作用,
注意
@Transactional注解也可以加在介面上,但只有在設定了基于介面的代理時才會生效,因為注解不能繼承,所以該注解最好是加在類的實作上,
1.2 不容忽視的例外體系
目標:Java例外體系(面試常問)與Spring事務存在什么聯系

結論:
Spring事務默認只回滾運行時例外和Error
偽代碼如下
@Transaction
public void insert(){
m_insert();
in_insert();//只有運行時例外 & Error 才可以回滾
}
Thorwable類是所有例外和錯誤的超類,有兩個子類Error和Exception,分別表示錯誤和例外,
例外類Exception又分為運行時例外(RuntimeException)和非運行時例外,這兩種例外有很大的區別,也稱之為不檢查例外(Unchecked Exception)和檢查例外(Checked Exception),
1、Error與Exception
Error是程式無法處理的錯誤,比如OutOfMemoryError、ThreadDeath等,這些例外發生時,Java虛擬機(JVM)一般會選擇執行緒終止,
Exception是程式本身可以處理的例外,這種例外分兩大類運行時例外和非運行時例外,程式中應當盡可能去處理這些例外,
2、運行時例外和非運行時例外
運行時例外都是RuntimeException類及其子類例外,如NullPointerException、IndexOutOfBoundsException等,這些例外是不檢查例外,程式中可以選擇捕獲處理,也可以不處理,
這些例外一般是由程式邏輯錯誤引起的,程式應該從邏輯角度盡可能避免這類例外的發生,
非運行時例外是RuntimeException以外的例外,型別上都屬于Exception類及其子類,從程式語法角度講是必須進行處理的例外,如果不處理,程式就不能編譯通過,如IOException、SQLException等以及用戶自定義的Exception例外,一般情況下不自定義檢查例外,
1.3 事務傳播行為與隔離級別
1.3.1 事務傳播行為
什么是事務傳播
事務傳播用來描述由某一個事務傳播行為修飾的方法被嵌套進另一個方法的時事務如何傳播,

tips
兩大類【有事務的情況】【沒事務的情況】
代碼理解概念
@Transactional
private void method5_Test() throws Exception {
System.out.println(">>>>>>>>>>>進入到業務方法");
User user = new User();
user.setName("張三");
userMapper.insertUser(user);
logService.insert(getLogEntity());
throw new RuntimeException();
}
.............LogService.java
@Override
// @Transactional(propagation = Propagation.NOT_SUPPORTED)
@Transactional
public void insert(Log log) throws Exception {
System.out.println(">>>>>>>>>>>進入到日志方法");
// Log log = new Log();
// log.setName("業務日志記錄");
logMapper.insertLog(log);
}
1.3.2 事務隔離級別
什么是事務的隔離
5大類
隔離性是指多個用戶的并發事務訪問同一個資料庫時,一個用戶的事務不應該被其他用戶的事務干擾,多個并發事務之間要相互隔
| 隔離級別 | 說明 |
|---|---|
| TransactionDefinition.ISOLATION_DEFAULT(默認) | PlatformTransactionManager的默認隔離級別,使用資料庫默認的事務隔離級別,另外四個與JDBC的隔離級別相對應, |
| TransactionDefinition.ISOLATION_READ_UNCOMMITTED(讀未提交) | 這是事務最低的隔離級別,它允許另外一個事務可以看到這個事務未提交的資料,這種隔離級別會產生臟讀,不可重復讀和幻像讀 |
| TransactionDefinition.ISOLATION_READ_COMMITTED(讀已提交) | 保證一個事務修改的資料提交后才能被另外一個事務讀取,另外一個事務不能讀取該事務未提交的資料,這種事務隔離級別可以避免臟讀出現,但是可能會出現不可重復讀和幻像讀, |
| TransactionDefinition.ISOLATION_REPEATABLE_READ(可重復讀) | 這種事務隔離級別可以防止臟讀、不可重復讀,但是可能出現幻像讀,它除了保證一個事務不能讀取另一個事務未提交的資料外,還保證了不可重復讀 |
| TransactionDefinition. ISOLATION_SERIALIZABLE(串行化) | 代價最大、可靠性最高的隔離級別,所有的事務都是按順序一個接一個地執行 |
如果不考慮隔離性,會發生什么事呢?
臟讀:
臟讀是指一個事務在處理資料的程序中,讀取到另一個為提交事務的資料
不可重復讀:
不可重復讀是指對于資料庫中的某個資料,一個事務范圍內的多次查詢卻回傳了不同的結果,這是由于在查詢程序中,資料被另外一個事務修改并提交了,
幻讀
幻讀是事務非獨立執行時發生的一種現象,例如事務T1對一個表中所有的行的某個資料項做了從“1”修改為“2”的操作,這時事務T2又對這個表中插入了一行資料項,而這個資料項的數值還是為“1”并且提交給資料庫,而操作事務T1的用戶如果再查看剛剛修改的資料,會發現還有一行沒有修改,其實這行是從事務T2中添加的,就好像產生幻覺一樣,這就是發生了幻讀
1.讀未提交(Read uncommitted):
這種事務隔離級別下,select陳述句不加鎖,
此時,可能讀取到不一致的資料,即“讀臟 ”,這是并發最高,一致性最差的隔離級別,
2.讀已提交(Read committed):
可避免 臟讀 的發生,
在互聯網大資料量,高并發量的場景下,幾乎 不會使用 上述兩種隔離級別,
3.可重復讀(Repeatable read):
MySql默認隔離級別,
可避免 臟讀 、不可重復讀 的發生,
4.串行化(Serializable ):
可避免 臟讀、不可重復讀、幻讀 的發生
1.4 Spring事務原始碼深度剖析
1.4.1 Spring事務環境介紹
目標:事務測驗環境介紹

1.4.2 事務是何時被織入的
目標:事務在Spring哪個階段織入的
思考:
程式在運行的時候【userService】為什么是代理物件

需要解決的問題:
1、代理物件是如何生成的
2、代理物件如何呼叫到了invoke方法
1.4.3 事務原始碼入口在哪里
1)事務三大介面介紹
目標:了解非常核心的事務三大介面
Spring事務三大介面介紹
1、PlatformTransactionManager: (平臺)事務管理器介面
PlatformTransactionManager 介面是 Spring 提供的平臺事務管理器頂級介面,用于管理事務,

Spring并不直接管理事務,而是提供了多種事務管理器;他們將事務管理的職責委托給Hibernate或者JTA等持久化機制的事務框架來實作
通過這個介面,Spring為各個平臺,比如JDBC等都提供了對應的事務管理器;但是具體實作就是下游事務框架了
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition):用于獲取事務狀態資訊,
void commit(TransactionStatus status):用于提交事務,
void rollback(TransactionStatus status):用于回滾事務,
}
該介面中提供了三個事務操作方法,具體如下,
- TransactionStatus getTransaction(TransactionDefinition definition):用于獲取事務狀態資訊,
- void commit(TransactionStatus status):用于提交事務,
- void rollback(TransactionStatus status):用于回滾事務,
下面是 PlatformTransactionManager各種實作

2、TransactionDefinition:事務定義資訊介面
比如:事務傳播行為、隔離級別、超時、只讀、回滾規則)
//7大傳播+5大隔離+超時、只讀、回滾
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;//默認:如果存在一個事務,則支持當前事務,如果沒有事務則開啟一個新的事務
.....略
}
3、TransactionStatus:事務運行狀態介面
public interface TransactionStatus extends SavepointManager, Flushable {
boolean isNewTransaction();//獲取是否是新事務
....略
}
TransactionStatus 介面是事務的狀態,它描述了某一時間點上事務的狀態資訊,其中包含六個操作
| 名稱 | 說明 |
|---|---|
| void flush() | 重繪事務 |
| boolean hasSavepoint() | 獲取是否存在保存點 |
| boolean isCompleted() | 獲取事務是否完成 |
| boolean isNewTransaction() | 獲取是否是新事務 |
| boolean isRollbackOnly() | 獲取是否回滾 |
| void setRollbackOnly() | 設定事務回滾 |
2)事務原始碼呼叫入口
目標:找到Spring事務的呼叫入口

tips
入口攔截器org.springframework.transaction.interceptor.TransactionInterceptor#invoke
TransactionInterceptor原始碼如下
// 獲取目標類
@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// 開始呼叫父類方法
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
Transactional 注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
// 傳播行為
// 一個開啟了事務的方法A,呼叫了另一個開啟了事務的方法B,此時會出現什么情況?這就要看傳播行為的設定了
Propagation propagation() default Propagation.REQUIRED;
//isolation屬性是用來設定事務的隔離級別,資料庫有四種隔離級別:
//讀未提交、讀已提交、可重復讀、可串行化,MySQL的默認隔離級別是可重復讀
Isolation isolation() default Isolation.DEFAULT;
//timtout是用來設定事務的超時時間,可以看到默認為-1,不會超時,
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
//? readOnly屬性用來設定該屬性是否是只讀事務,只讀事務要從兩方面來理解:
// 它的功能是設定了只讀事務后在整個事務的程序中,其他事務提交的內容對當前事務是不可見的
// 只讀事務中只能有讀操作,不能含有寫操作,否則會報錯
boolean readOnly() default false;
//當方法內拋出指定的例外時,進行事務回滾,默認情況下只對RuntimeException回滾,
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
// 用來設定出現指定的例外時,不進行回滾,
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
1.4.4 Spring事務原始碼深入剖析
1)事務正常執行流程
目標:
1、正常流程測驗(不拋運行時例外)
2、有運行時例外的情況
測驗代碼
//實作類
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
@Transactional
public void insert() throws Exception {
method2_Test();//正常情況下執行
method3_Test() ;//有運行時例外的情況
}
}
找到Spring事務攔截器入口
org.springframework.transaction.interceptor.TransactionInterceptor#invoke
// 獲取目標類
@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// 開始呼叫父類方法
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
進入到invokeWithinTransaction
tips:具體流程
1、獲取事務屬性
2、創建事務
3、呼叫目標方法
4、回滾 事務 or 提交事務
// 包含了事務執行的整個流程,這里是使用了模板模式,具體的實作交給子類去實作
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// 獲取事務屬性,如果事務屬性為空,則沒有事務
TransactionAttributeSource tas = getTransactionAttributeSource();
//繼承TransactionDefinition
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 獲取實作:DataSourceTransactionManager 管理 JDBC 的 Connection,
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 切點標識-->com.tx.test.impl.UserServiceImpl.insert
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// TODO: 創建(開啟)事務(根據事務的傳播行為屬性去判斷是否創建一個事務)
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// TODO: 呼叫目標方法
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// TODO: 回滾事務 目標方法呼叫發生了例外(后置增強),
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// 清理資訊
cleanupTransactionInfo(txInfo);
}
//TODO: 提交事務
commitTransactionAfterReturning(txInfo);
return retVal;
.............略

TransactionAttribute繼承了TransactionDefinition
提交
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" + con + "]");
}
try {
//TODO :jdbc鏈接提交事務
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}
回滾
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 {
//TODO jdbc回滾事務
con.rollback();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
}
}
總結
1、獲取事務屬性
2、創建事務
1、獲取事務管理器DataSourceTransactionManager(通過資料源、拿到鏈接、在設定事務管理器)
2、判斷是否存在事務,如果有就去判斷傳播屬性!!!(第一次不存在)
3、不存在事務的情況
如果事務超時時間小于默認(-1)或者沒有事務,則拋出例外
新建一個事務(doBegin開啟事務)
注意:第一次進入則是新建一個事務
3、呼叫目標方法
4、回滾 事務 or 提交事務
提交事務
1、呼叫doCommit提交事務
通過事務管理器物件DataSourceTransactionObject拿到con執行con.commit()
回滾事務
1、呼叫doRollback回滾
通過事務管理器物件DataSourceTransactionObject拿到con執行con.rollback()
2)生產事務失效之謎
目標:
1、事務失效的原因是什么
2、如何解決事務失效
3、在原始碼中什么地方判斷的
測驗代碼
@Override
@Transactional
public void insert() throws Exception {
method4_Test();//事務失效
}
解決方案
@Transactional(rollbackFor = Exception.class)
重點關注原始碼
org.springframework.transaction.interceptor.TransactionAspectSupport#completeTransactionAfterThrowing中的rollbackOn方法
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
//TODO rollbackOn獲取回滾規則;可以自定義設定回滾規則,默認會判斷RuntimeException和Error,!!!!!!!
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
// TODO 回滾事務
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
} catch (TransactionSystemException ex2) {
................略
3)線上日志資料丟失
目標:通過傳播屬性解決業務中日志丟失問題

測驗代碼入口
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:application.xml");
UserService userService = (UserService) context.getBean("userService");
// LogService logService = (LogService) context.getBean("logService");
try {
userService.insert();//用戶
// logService.insert();//日志
} catch (Exception e) {
e.printStackTrace();
}
}
}
日志插入插入失敗
使用默認的傳播屬性和隔離級別
//實作類
public class LogServiceImpl implements LogService {
@Autowired
private LogMapper logMapper;
@Override
// @Transactional(propagation = Propagation.NOT_SUPPORTED)
@Transactional
public void insert(Log log) throws Exception {
System.out.println(">>>>>>>>>>>進入到日志方法");
// Log log = new Log();
// log.setName("業務日志記錄");
logMapper.insertLog(log);
}
tips
@Transactional(propagation = Propagation.NOT_SUPPORTED)
這樣修改則修改成功
用戶資訊插入,呼叫日志插入(
@Override
@Transactional
// @Transactional(rollbackFor = Exception.class)
public void insert() throws Exception {
method5_Test();
}
總結
1、現象
? 用戶插入的時候;呼叫log的service插入;如果出現例外;兩者全部回滾
? 也就是用戶插入失敗、日志插入失敗
2、需求
? 正常的業務情況;都是在事務失敗的時候;同時會要求日志也要插入成功
3、程序
? 目前;用戶和日志的service使用的都是默認的傳播屬性和隔離級別
4、改進
? 將日志的傳播屬性修改成Propagation.NOT_SUPPORTED【如果當前存在事務;就掛起當前事務】
?
本文由
傳智教育博學谷教研團隊發布,如果本文對您有幫助,歡迎
關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力,轉載請注明出處!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/521785.html
標籤:Java
