程序員新人周一優(yōu)化一行代碼,周三被勸退?
點擊藍色“程序員黃小斜”關注我喲
加個“星標”,每天和你一起多進步一點點!

如若轉載請聯(lián)系原公眾號
這周一,公司新來了一個同事,面試的時候表現(xiàn)得非常不錯,各種問題對答如流,老板和我都倍感欣慰。
這么優(yōu)秀的人,絕不能讓他浪費一分一秒,于是很快,我就發(fā)他了需求文檔、源碼,讓他先在本地熟悉一下業(yè)務和開發(fā)流程。
結果沒想到,周三大家一塊 review 代碼的時候就發(fā)現(xiàn)了問題,新來的同事直接把原來 @Transactional 優(yōu)化成了這個鬼樣子:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
就因為這一行代碼,老板(當年也是一線互聯(lián)網(wǎng)大廠的好手)當場就發(fā)飆了,馬上就要勸退這位新同事,我就趕緊打圓場,畢竟自己面試的人,不看僧面看佛面,是吧?于是老板答應我說再試用一個月看看。
會議結束后,我就趕緊讓新同事復習了一遍事務,以下是他自己做的總結,還是非常詳細的,分享出來給大家一點點參考和啟發(fā)。相信大家看完后就明白為什么不能這樣優(yōu)化 @Transactional 注解了,純屬畫蛇添足和亂用。
關于事務
事務在邏輯上是一組操作,要么執(zhí)行,要不都不執(zhí)行。主要是針對數(shù)據(jù)庫而言的,比如說 MySQL。
只要記住這一點,理解事務就很容易了。在 Java 中,我們通常要在業(yè)務里面處理多個事件,比如說編程喵??有一個保存文章的方法,它除了要保存文章本身之外,還要保存文章對應的標簽,標簽和文章不在同一個表里,但會通過在文章表里(posts)保存標簽主鍵(tag_id)來關聯(lián)標簽表(tags):
public void savePosts(PostsParam postsParam) {
// 保存文章
save(posts);
// 處理標簽
insertOrUpdateTag(postsParam, posts);
}
那么此時就需要開啟事務,保證文章表和標簽表中的數(shù)據(jù)保持同步,要么都執(zhí)行,要么都不執(zhí)行。
否則就有可能造成,文章保存成功了,但標簽保存失敗了,或者文章保存失敗了,標簽保存成功了——這些場景都不符合我們的預期。
為了保證事務是正確可靠的,在數(shù)據(jù)庫進行寫入或者更新操作時,就必須得表現(xiàn)出 ACID 的 4 個重要特性:
原子性(Atomicity):一個事務中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環(huán)節(jié)。事務在執(zhí)行過程中發(fā)生錯誤,會被回滾(Rollback)到事務開始前的狀態(tài),就像這個事務從來沒有執(zhí)行過一樣。 一致性(Consistency):在事務開始之前和事務結束以后,數(shù)據(jù)庫的完整性沒有被破壞。 事務隔離(Isolation):數(shù)據(jù)庫允許多個并發(fā)事務同時對其數(shù)據(jù)進行讀寫和修改,隔離性可以防止多個事務并發(fā)執(zhí)行時由于交叉執(zhí)行而導致數(shù)據(jù)的不一致。 持久性(Durability):事務處理結束后,對數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會丟失。
其中,事務隔離又分為 4 種不同的級別,包括:
未提交讀(Read uncommitted),最低的隔離級別,允許“臟讀”(dirty reads),事務可以看到其他事務“尚未提交”的修改。如果另一個事務回滾,那么當前事務讀到的數(shù)據(jù)就是臟數(shù)據(jù)。 提交讀(read committed),一個事務可能會遇到不可重復讀(Non Repeatable Read)的問題。不可重復讀是指,在一個事務內(nèi),多次讀同一數(shù)據(jù),在這個事務還沒有結束時,如果另一個事務恰好修改了這個數(shù)據(jù),那么,在第一個事務中,兩次讀取的數(shù)據(jù)就可能不一致。 可重復讀(repeatable read),一個事務可能會遇到幻讀(Phantom Read)的問題。幻讀是指,在一個事務中,第一次查詢某條記錄,發(fā)現(xiàn)沒有,但是,當試圖更新這條不存在的記錄時,竟然能成功,并且,再次讀取同一條記錄,它就神奇地出現(xiàn)了。 串行化(Serializable),最嚴格的隔離級別,所有事務按照次序依次執(zhí)行,因此,臟讀、不可重復讀、幻讀都不會出現(xiàn)。雖然 Serializable 隔離級別下的事務具有最高的安全性,但是,由于事務是串行執(zhí)行,所以效率會大大下降,應用程序的性能會急劇降低。如果沒有特別重要的情景,一般都不會使用 Serializable 隔離級別。
需要格外注意的是:事務能否生效,取決于數(shù)據(jù)庫引擎是否支持事務,MySQL 的 InnoDB 引擎是支持事務的,但 MyISAM 就不支持。
關于 Spring 對事務的支持
Spring 支持兩種事務方式,分別是編程式事務和聲明式事務,后者最常見,通常情況下只需要一個 @Transactional 就搞定了(代碼侵入性降到了最低),就像這樣:
@Transactional
public void savePosts(PostsParam postsParam) {
// 保存文章
save(posts);
// 處理標簽
insertOrUpdateTag(postsParam, posts);
}
1)編程式事務
編程式事務是指將事務管理代碼嵌入嵌入到業(yè)務代碼中,來控制事務的提交和回滾。
你比如說,使用 TransactionTemplate 來管理事務:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 業(yè)務代碼
} catch (Exception e){
//回滾
transactionStatus.setRollbackOnly();
}
}
});
}
再比如說,使用 TransactionManager 來管理事務:
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 業(yè)務代碼
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
就編程式事務管理而言,Spring 更推薦使用 TransactionTemplate。
在編程式事務中,必須在每個業(yè)務操作中包含額外的事務管理代碼,就導致代碼看起來非常的臃腫,但對理解 Spring 的事務管理模型非常有幫助。
2)聲明式事務
聲明式事務將事務管理代碼從業(yè)務方法中抽離了出來,以聲明式的方式來實現(xiàn)事務管理,對于開發(fā)者來說,聲明式事務顯然比編程式事務更易用、更好用。
當然了,要想實現(xiàn)事務管理和業(yè)務代碼的抽離,就必須得用到 Spring 當中最關鍵最核心的技術之一,AOP,其本質(zhì)是對方法前后進行攔截,然后在目標方法開始之前創(chuàng)建或者加入一個事務,執(zhí)行完目標方法之后根據(jù)執(zhí)行的情況提交或者回滾。
聲明式事務雖然優(yōu)于編程式事務,但也有不足,聲明式事務管理的粒度是方法級別,而編程式事務是可以精確到代碼塊級別的。
事務管理模型
Spring 將事務管理的核心抽象為一個事務管理器(TransactionManager),它的源碼只有一個簡單的接口定義,屬于一個標記接口:
public interface TransactionManager {
}
該接口有兩個子接口,分別是編程式事務接口 ReactiveTransactionManager 和聲明式事務接口 PlatformTransactionManager。我們來重點說說 PlatformTransactionManager,該接口定義了 3 個接口方法:
interface PlatformTransactionManager extends TransactionManager{
// 根據(jù)事務定義獲取事務狀態(tài)
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// 提交事務
void commit(TransactionStatus status) throws TransactionException;
// 事務回滾
void rollback(TransactionStatus status) throws TransactionException;
}
通過 PlatformTransactionManager 這個接口,Spring 為各個平臺如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了對應的事務管理器,但是具體的實現(xiàn)就是各個平臺自己的事情了。
參數(shù) TransactionDefinition 和 @Transactional 注解是對應的,比如說 @Transactional 注解中定義的事務傳播行為、隔離級別、事務超時時間、事務是否只讀等屬性,在 TransactionDefinition 都可以找得到。
返回類型 TransactionStatus 主要用來存儲當前事務的一些狀態(tài)和數(shù)據(jù),比如說事務資源(connection)、回滾狀態(tài)等。
TransactionDefinition.java:
public interface TransactionDefinition {
// 事務的傳播行為
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
// 事務的隔離級別
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
// 事務超時時間
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
// 事務是否只讀
default boolean isReadOnly() {
return false;
}
}
Transactional.java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
}
@Transactional 注解中的 propagation 對應 TransactionDefinition 中的 getPropagationBehavior,默認值為 Propagation.REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)。@Transactional 注解中的 isolation 對應 TransactionDefinition 中的 getIsolationLevel,默認值為 DEFAULT(TransactionDefinition.ISOLATION_DEFAULT)。@Transactional 注解中的 timeout 對應 TransactionDefinition 中的 getTimeout,默認值為TransactionDefinition.TIMEOUT_DEFAULT。 @Transactional 注解中的 readOnly 對應 TransactionDefinition 中的 isReadOnly,默認值為 false。
說到這,我們來詳細地說明一下 Spring 事務的傳播行為、事務的隔離級別、事務的超時時間、事務的只讀屬性,以及事務的回滾規(guī)則。
事務傳播行為
當事務方法被另外一個事務方法調(diào)用時,必須指定事務應該如何傳播,例如,方法可能繼續(xù)在當前事務中執(zhí)行,也可以開啟一個新的事務,在自己的事務中執(zhí)行。
聲明式事務的傳播行為可以通過 @Transactional 注解中的 propagation 屬性來定義,比如說:
@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}
TransactionDefinition 一共定義了 7 種事務傳播行為:
01、PROPAGATION_REQUIRED
這也是 @Transactional 默認的事務傳播行為,指的是如果當前存在事務,則加入該事務;如果當前沒有事務,則創(chuàng)建一個新的事務。更確切地意思是:
如果外部方法沒有開啟事務的話,Propagation.REQUIRED 修飾的內(nèi)部方法會開啟自己的事務,且開啟的事務相互獨立,互不干擾。 如果外部方法開啟事務并且是 Propagation.REQUIRED 的話,所有 Propagation.REQUIRED 修飾的內(nèi)部方法和外部方法均屬于同一事務 ,只要一個方法回滾,整個事務都需要回滾。
Class A {
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}
Class B {
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void bMethod {
//do something
}
}
這個傳播行為也最好理解,aMethod 調(diào)用了 bMethod,只要其中一個方法回滾,整個事務均回滾。
02、PROPAGATION_REQUIRES_NEW
創(chuàng)建一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說不管外部方法是否開啟事務,Propagation.REQUIRES_NEW 修飾的內(nèi)部方法都會開啟自己的事務,且開啟的事務與外部的事務相互獨立,互不干擾。
Class A {
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void aMethod {
//do something
B b = new B();
b.bMethod();
}
}
Class B {
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void bMethod {
//do something
}
}
如果 aMethod()發(fā)生異常回滾,bMethod()不會跟著回滾,因為 bMethod()開啟了獨立的事務。但是,如果 bMethod()拋出了未被捕獲的異常并且這個異常滿足事務回滾規(guī)則的話,aMethod()同樣也會回滾。
03、PROPAGATION_NESTED
如果當前存在事務,就在當前事務內(nèi)執(zhí)行;否則,就執(zhí)行與 PROPAGATION_REQUIRED 類似的操作。
04、PROPAGATION_MANDATORY
如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。
05、PROPAGATION_SUPPORTS
如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續(xù)運行。
06、PROPAGATION_NOT_SUPPORTED
以非事務方式運行,如果當前存在事務,則把當前事務掛起。
07、PROPAGATION_NEVER
以非事務方式運行,如果當前存在事務,則拋出異常。
3、4、5、6、7 這 5 種事務傳播方式不常用,了解即可。
事務隔離級別
前面我們已經(jīng)了解了數(shù)據(jù)庫的事務隔離級別,再來理解 Spring 的事務隔離級別就容易多了。
TransactionDefinition 中一共定義了 5 種事務隔離級別:
ISOLATION_DEFAULT,使用數(shù)據(jù)庫默認的隔離級別,MySql 默認采用的是 REPEATABLE_READ,也就是可重復讀。 ISOLATION_READ_UNCOMMITTED,最低的隔離級別,可能會出現(xiàn)臟讀、幻讀或者不可重復讀。 ISOLATION_READ_COMMITTED,允許讀取并發(fā)事務提交的數(shù)據(jù),可以防止臟讀,但幻讀和不可重復讀仍然有可能發(fā)生。 ISOLATION_REPEATABLE_READ,對同一字段的多次讀取結果都是一致的,除非數(shù)據(jù)是被自身事務所修改的,可以阻止臟讀和不可重復讀,但幻讀仍有可能發(fā)生。 ISOLATION_SERIALIZABLE,最高的隔離級別,雖然可以阻止臟讀、幻讀和不可重復讀,但會嚴重影響程序性能。
通常情況下,我們采用默認的隔離級別 ISOLATION_DEFAULT 就可以了,也就是交給數(shù)據(jù)庫來決定,可以通過 SELECT @@transaction_isolation; 命令來查看 MySql 的默認隔離級別,結果為 REPEATABLE-READ,也就是可重復讀。

事務的超時時間
事務超時,也就是指一個事務所允許執(zhí)行的最長時間,如果在超時時間內(nèi)還沒有完成的話,就自動回滾。
假如事務的執(zhí)行時間格外的長,由于事務涉及到對數(shù)據(jù)庫的鎖定,就會導致長時間運行的事務占用數(shù)據(jù)庫資源。
事務的只讀屬性
如果一個事務只是對數(shù)據(jù)庫執(zhí)行讀操作,那么該數(shù)據(jù)庫就可以利用事務的只讀屬性,采取優(yōu)化措施,適用于多條數(shù)據(jù)庫查詢操作中。
為什么一個查詢操作還要啟用事務支持呢?
這是因為 MySql(innodb)默認對每一個連接都啟用了 autocommit 模式,在該模式下,每一個發(fā)送到 MySql 服務器的 SQL 語句都會在一個單獨的事務中進行處理,執(zhí)行結束后會自動提交事務。
那如果我們給方法加上了 @Transactional 注解,那這個方法中所有的 SQL 都會放在一個事務里。否則,每條 SQL 都會單獨開啟一個事務,中間被其他事務修改了數(shù)據(jù),都會實時讀取到。
有些情況下,當一次執(zhí)行多條查詢語句時,需要保證數(shù)據(jù)一致性時,就需要啟用事務支持。否則上一條 SQL 查詢后,被其他用戶改變了數(shù)據(jù),那么下一個 SQL 查詢可能就會出現(xiàn)不一致的狀態(tài)。
事務的回滾策略
默認情況下,事務只在出現(xiàn)運行時異常(Runtime Exception)時回滾,以及 Error,出現(xiàn)檢查異常(checked exception,需要主動捕獲處理或者向上拋出)時不回滾。
https://tobebetterjavaer.com/exception/gailan.html
如果你想要回滾特定的異常類型的話,可以這樣設置:
@Transactional(rollbackFor= MyException.class)
關于 Spring Boot 對事務的支持
以前,我們需要通過 XML 配置 Spring 來托管事務,有了 Spring Boot 之后,一切就變得更加簡單了,只需要在業(yè)務層添加事務注解(@Transactional)就可以快速開啟事務。
也就是說,我們只需要把焦點放在 @Transactional 注解上就可以了。
@Transactional 的作用范圍
類上,表明類中所有 public 方法都啟用事務 方法上,最常用的一種 接口上,不推薦使用
@Transactional 的常用配置參數(shù)
雖然 @Transactional 注解源碼中定義了很多屬性,但大多數(shù)時候,我都是采用默認配置,當然了,如果需要自定義的話,前面也都說明過了。
@Transactional 的使用注意事項總結
1)要在 public 方法上使用,在AbstractFallbackTransactionAttributeSource類的computeTransactionAttribute方法中有個判斷,如果目標方法不是public,則TransactionAttribute返回null,即不支持事務。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
2)避免同一個類中調(diào)用 @Transactional 注解的方法,這樣會導致事務失效。
測試事務是否起效
在測試之前,我們先把 Spring Boot 默認的日志級別 info 調(diào)整為 debug,在 application.yml 文件中 修改:
logging:
level:
org:
hibernate: debug
springframework:
web: debug
然后,來看修改之前查到的數(shù)據(jù):

開搞。在控制器中添加一個 update 接口,準備修改數(shù)據(jù),打算把沉默王二的狗腿子修改為沉默王二的狗腿:
@RequestMapping("/update")
public String update(Model model) {
User user = userService.findById(2);
user.setName("沉默王二的狗腿");
userService.update(user);
return "update";
}
在 Service 中為方法加上 @Transactional 注解并拋出運行時異常:
@Override
@Transactional
public void update(User user) {
userRepository.save(user);
throw new RuntimeException("啊,出現(xiàn)妖怪了!");
}
按照我們的預期,當執(zhí)行 save 保存數(shù)據(jù)后,因為出現(xiàn)了異常,所以事務要回滾。所以數(shù)據(jù)不會被修改。
在瀏覽器中輸入 http://localhost:8080/user/update 進行測試,注意查看日志,可以確認事務起效了。

當我們把事務去掉,同樣拋出異常:
@Override
public void update(User user) {
userRepository.save(user);
throw new RuntimeException("啊,出現(xiàn)妖怪了!");
}
再次執(zhí)行,發(fā)現(xiàn)雖然程序報錯了,但數(shù)據(jù)卻被更新了。

這也間接地證明,我們的 @Transactional 事務起效了。
看到這,是不是就明白為什么新同事的優(yōu)化純屬畫蛇添足/卵用了吧?
項目源碼
編程喵:https://github.com/itwanger/coding-more 本項目源碼:https://github.com/itwanger/codingmore-learning
參考來源:
維基百科:https://zh.wikipedia.org/wiki/ACID 維基百科:https://zh.wikipedia.org/wiki/事務隔離 廖雪峰:https://www.liaoxuefeng.com/wiki/1177760294764384/1179611198786848 JavaGuide:https://juejin.cn/post/6844903608224333838 全菜工程師小輝:https://aijishu.com/a/1060000000013284 空無:https://segmentfault.com/a/1190000040130617 一只襪子:https://www.jianshu.com/p/380a9d980ca5
40 個 SpringBoot 常用注解:讓生產(chǎn)力爆表!
除了Navicat:正版 MySQL 客戶端,真香!
用數(shù)據(jù)告訴你高考最難的省份是哪里!
— 【 THE END 】— 公眾號[程序員黃小斜]全部博文已整理成一個目錄,請在公眾號里回復「m」獲取! 最近面試BAT,整理一份面試資料《Java面試BATJ通關手冊》,覆蓋了Java核心技術、JVM、Java并發(fā)、SSM、微服務、數(shù)據(jù)庫、數(shù)據(jù)結構等等。
獲取方式:點“在看”,關注公眾號并回復 PDF 領取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉發(fā)吧。
謝謝支持喲 (*^__^*)




