当前位置:网站首页>程序員新人周一優化一行代碼,周三被勸退?

程序員新人周一優化一行代碼,周三被勸退?

2022-06-21 09:40:00 程序員小灰

445accd2402c32fc94bf1a418b1ca90d.png

本文經沉默王二(id:cmower)授權轉載

如若轉載請聯系原公眾號

這周一,公司新來了一個同事,面試的時候錶現得非常不錯,各種問題對答如流,老板和我都倍感欣慰。

這麼優秀的人,絕不能讓他浪費一分一秒,於是很快,我就發他了需求文檔、源碼,讓他先在本地熟悉一下業務和開發流程。

結果沒想到,周三大家一起 review 代碼的時候就發現了問題,新來的同事直接把原來 @Transactional 優化成了這個鬼樣子:

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)

就因為這一行代碼,老板(當年也是一線互聯網大廠的好手)當場就發飆了,馬上就要勸退這比特新同事,我就趕緊打圓場,畢竟自己面試的人,不看僧面看佛面,是吧?於是老板答應我說再試用一個月看看。

會議結束後,我就趕緊讓新同事複習了一遍事務,以下是他自己做的總結,還是非常詳細的,分享出來給大家一點點參考和啟發。相信大家看完後就明白為什麼不能這樣優化 @Transactional 注解了,純屬畫蛇添足和亂用。


關於事務

事務在邏輯上是一組操作,要麼執行,要不都不執行。主要是針對數據庫而言的,比如說 MySQL。

只要記住這一點,理解事務就很容易了。在 Java 中,我們通常要在業務裏面處理多個事件,比如說編程喵有一個保存文章的方法,它除了要保存文章本身之外,還要保存文章對應的標簽,標簽和文章不在同一個錶裏,但會通過在文章錶裏(posts)保存標簽主鍵(tag_id)來關聯標簽錶(tags):

public void savePosts(PostsParam postsParam) {
 // 保存文章
 save(posts);
 // 處理標簽
  insertOrUpdateTag(postsParam, posts);
}

那麼此時就需要開啟事務,保證文章錶和標簽錶中的數據保持同步,要麼都執行,要麼都不執行。

否則就有可能造成,文章保存成功了,但標簽保存失敗了,或者文章保存失敗了,標簽保存成功了——這些場景都不符合我們的預期。

為了保證事務是正確可靠的,在數據庫進行寫入或者更新操作時,就必須得錶現出 ACID 的 4 個重要特性:

  • 原子性(Atomicity):一個事務中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

  • 一致性(Consistency):在事務開始之前和事務結束以後,數據庫的完整性沒有被破壞。

  • 事務隔離(Isolation):數據庫允許多個並發事務同時對其數據進行讀寫和修改,隔離性可以防止多個事務並發執行時由於交叉執行而導致數據的不一致。

  • 持久性(Durability):事務處理結束後,對數據的修改就是永久的,即便系統故障也不會丟失。

其中,事務隔離又分為 4 種不同的級別,包括:

  • 未提交讀(Read uncommitted),最低的隔離級別,允許“髒讀”(dirty reads),事務可以看到其他事務“尚未提交”的修改。如果另一個事務回滾,那麼當前事務讀到的數據就是髒數據。

  • 提交讀(read committed),一個事務可能會遇到不可重複讀(Non Repeatable Read)的問題。不可重複讀是指,在一個事務內,多次讀同一數據,在這個事務還沒有結束時,如果另一個事務恰好修改了這個數據,那麼,在第一個事務中,兩次讀取的數據就可能不一致。

  • 可重複讀(repeatable read),一個事務可能會遇到幻讀(Phantom Read)的問題。幻讀是指,在一個事務中,第一次查詢某條記錄,發現沒有,但是,當試圖更新這條不存在的記錄時,竟然能成功,並且,再次讀取同一條記錄,它就神奇地出現了。

  • 串行化(Serializable),最嚴格的隔離級別,所有事務按照次序依次執行,因此,髒讀、不可重複讀、幻讀都不會出現。雖然 Serializable 隔離級別下的事務具有最高的安全性,但是,由於事務是串行執行,所以效率會大大下降,應用程序的性能會急劇降低。如果沒有特別重要的情景,一般都不會使用 Serializable 隔離級別。

需要格外注意的是:事務能否生效,取决於數據庫引擎是否支持事務,MySQL 的 InnoDB 引擎是支持事務的,但 MyISAM 就不支持

關於 Spring 對事務的支持

Spring 支持兩種事務方式,分別是編程式事務和聲明式事務,後者最常見,通常情况下只需要一個 @Transactional 就搞定了(代碼侵入性降到了最低),就像這樣:

@Transactional
public void savePosts(PostsParam postsParam) {
 // 保存文章
 save(posts);
 // 處理標簽
  insertOrUpdateTag(postsParam, posts);
}

1)編程式事務

編程式事務是指將事務管理代碼嵌入嵌入到業務代碼中,來控制事務的提交和回滾。

你比如說,使用 TransactionTemplate 來管理事務:

@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

                try {

                    // ....  業務代碼
                } catch (Exception e){
                    //回滾
                    transactionStatus.setRollbackOnly();
                }

            }
        });
}

再比如說,使用 TransactionManager 來管理事務:

@Autowired
private PlatformTransactionManager transactionManager;

public void testTransaction() {

  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
          try {
               // ....  業務代碼
              transactionManager.commit(status);
          } catch (Exception e) {
              transactionManager.rollback(status);
          }
}

就編程式事務管理而言,Spring 更推薦使用 TransactionTemplate。

在編程式事務中,必須在每個業務操作中包含額外的事務管理代碼,就導致代碼看起來非常的臃腫,但對理解 Spring 的事務管理模型非常有幫助。

2)聲明式事務

聲明式事務將事務管理代碼從業務方法中抽離了出來,以聲明式的方式來實現事務管理,對於開發者來說,聲明式事務顯然比編程式事務更易用、更好用。

當然了,要想實現事務管理和業務代碼的抽離,就必須得用到 Spring 當中最關鍵最核心的技術之一,AOP,其本質是對方法前後進行攔截,然後在目標方法開始之前創建或者加入一個事務,執行完目標方法之後根據執行的情况提交或者回滾。

聲明式事務雖然優於編程式事務,但也有不足,聲明式事務管理的粒度是方法級別,而編程式事務是可以精確到代碼塊級別的

事務管理模型

Spring 將事務管理的核心抽象為一個事務管理器(TransactionManager),它的源碼只有一個簡單的接口定義,屬於一個標記接口:

public interface TransactionManager {

}

該接口有兩個子接口,分別是編程式事務接口 ReactiveTransactionManager 和聲明式事務接口 PlatformTransactionManager。我們來重點說說 PlatformTransactionManager,該接口定義了 3 個接口方法:

interface PlatformTransactionManager extends TransactionManager{
    // 根據事務定義獲取事務狀態
    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)等都提供了對應的事務管理器,但是具體的實現就是各個平臺自己的事情了。

參數 TransactionDefinition 和 @Transactional 注解是對應的,比如說 @Transactional 注解中定義的事務傳播行為、隔離級別、事務超時時間、事務是否只讀等屬性,在 TransactionDefinition 都可以找得到。

返回類型 TransactionStatus 主要用來存儲當前事務的一些狀態和數據,比如說事務資源(connection)、回滾狀態等。

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 事務的傳播行為、事務的隔離級別、事務的超時時間、事務的只讀屬性,以及事務的回滾規則。

事務傳播行為

當事務方法被另外一個事務方法調用時,必須指定事務應該如何傳播,例如,方法可能繼續在當前事務中執行,也可以開啟一個新的事務,在自己的事務中執行。

聲明式事務的傳播行為可以通過 @Transactional 注解中的 propagation 屬性來定義,比如說:

@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}

TransactionDefinition 一共定義了 7 種事務傳播行為:

01、PROPAGATION_REQUIRED

這也是 @Transactional 默認的事務傳播行為,指的是如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。更確切地意思是:

  • 如果外部方法沒有開啟事務的話,Propagation.REQUIRED 修飾的內部方法會開啟自己的事務,且開啟的事務相互獨立,互不幹擾。

  • 如果外部方法開啟事務並且是 Propagation.REQUIRED 的話,所有 Propagation.REQUIRED 修飾的內部方法和外部方法均屬於同一事務 ,只要一個方法回滾,整個事務都需要回滾。

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 調用了 bMethod,只要其中一個方法回滾,整個事務均回滾。

02、PROPAGATION_REQUIRES_NEW

創建一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說不管外部方法是否開啟事務,Propagation.REQUIRES_NEW 修飾的內部方法都會開啟自己的事務,且開啟的事務與外部的事務相互獨立,互不幹擾。

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()發生异常回滾,bMethod()不會跟著回滾,因為 bMethod()開啟了獨立的事務。但是,如果 bMethod()拋出了未被捕獲的异常並且這個异常滿足事務回滾規則的話,aMethod()同樣也會回滾。

03、PROPAGATION_NESTED

如果當前存在事務,就在當前事務內執行;否則,就執行與 PROPAGATION_REQUIRED 類似的操作。

04、PROPAGATION_MANDATORY

如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出异常。

05、PROPAGATION_SUPPORTS

如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。

06、PROPAGATION_NOT_SUPPORTED

以非事務方式運行,如果當前存在事務,則把當前事務掛起。

07、PROPAGATION_NEVER

以非事務方式運行,如果當前存在事務,則拋出异常。

3、4、5、6、7 這 5 種事務傳播方式不常用,了解即可。

事務隔離級別

前面我們已經了解了數據庫的事務隔離級別,再來理解 Spring 的事務隔離級別就容易多了。

TransactionDefinition 中一共定義了 5 種事務隔離級別:

  • ISOLATION_DEFAULT,使用數據庫默認的隔離級別,MySql 默認采用的是 REPEATABLE_READ,也就是可重複讀。

  • ISOLATION_READ_UNCOMMITTED,最低的隔離級別,可能會出現髒讀、幻讀或者不可重複讀。

  • ISOLATION_READ_COMMITTED,允許讀取並發事務提交的數據,可以防止髒讀,但幻讀和不可重複讀仍然有可能發生。

  • ISOLATION_REPEATABLE_READ,對同一字段的多次讀取結果都是一致的,除非數據是被自身事務所修改的,可以阻止髒讀和不可重複讀,但幻讀仍有可能發生。

  • ISOLATION_SERIALIZABLE,最高的隔離級別,雖然可以阻止髒讀、幻讀和不可重複讀,但會嚴重影響程序性能。

通常情况下,我們采用默認的隔離級別 ISOLATION_DEFAULT 就可以了,也就是交給數據庫來决定,可以通過 SELECT @@transaction_isolation; 命令來查看 MySql 的默認隔離級別,結果為 REPEATABLE-READ,也就是可重複讀。

dd0a9e84754970bf938832759e75e7fe.png

事務的超時時間

事務超時,也就是指一個事務所允許執行的最長時間,如果在超時時間內還沒有完成的話,就自動回滾。

假如事務的執行時間格外的長,由於事務涉及到對數據庫的鎖定,就會導致長時間運行的事務占用數據庫資源。

事務的只讀屬性

如果一個事務只是對數據庫執行讀操作,那麼該數據庫就可以利用事務的只讀屬性,采取優化措施,適用於多條數據庫查詢操作中。

為什麼一個查詢操作還要啟用事務支持呢?

這是因為 MySql(innodb)默認對每一個連接都啟用了 autocommit 模式,在該模式下,每一個發送到 MySql 服務器的 SQL 語句都會在一個單獨的事務中進行處理,執行結束後會自動提交事務。

那如果我們給方法加上了 @Transactional 注解,那這個方法中所有的 SQL 都會放在一個事務裏。否則,每條 SQL 都會單獨開啟一個事務,中間被其他事務修改了數據,都會實時讀取到。

有些情况下,當一次執行多條查詢語句時,需要保證數據一致性時,就需要啟用事務支持。否則上一條 SQL 查詢後,被其他用戶改變了數據,那麼下一個 SQL 查詢可能就會出現不一致的狀態。

事務的回滾策略

默認情况下,事務只在出現運行時异常(Runtime Exception)時回滾,以及 Error,出現檢查异常(checked exception,需要主動捕獲處理或者向上拋出)時不回滾。

https://tobebetterjavaer.com/exception/gailan.html

如果你想要回滾特定的异常類型的話,可以這樣設置:

@Transactional(rollbackFor= MyException.class)

關於 Spring Boot 對事務的支持

以前,我們需要通過 XML 配置 Spring 來托管事務,有了 Spring Boot 之後,一切就變得更加簡單了,只需要在業務層添加事務注解(@Transactional)就可以快速開啟事務。

也就是說,我們只需要把焦點放在 @Transactional 注解上就可以了。

@Transactional 的作用範圍

  • 類上,錶明類中所有 public 方法都啟用事務

  • 方法上,最常用的一種

  • 接口上,不推薦使用

@Transactional 的常用配置參數

雖然 @Transactional 注解源碼中定義了很多屬性,但大多數時候,我都是采用默認配置,當然了,如果需要自定義的話,前面也都說明過了。

@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)避免同一個類中調用 @Transactional 注解的方法,這樣會導致事務失效。

更多事務失效的場景

測試事務是否起效

在測試之前,我們先把 Spring Boot 默認的日志級別 info 調整為 debug,在 application.yml 文件中 修改:

logging:
  level:
    org:
      hibernate: debug
      springframework:
        web: debug

然後,來看修改之前查到的數據:

ee7fd1e330cd6fcf18150b1ee0e2a8b0.png

開搞。在控制器中添加一個 update 接口,准備修改數據,打算把沉默王二的狗腿子修改為沉默王二的狗腿:

@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("啊,出現妖怪了!");
}

按照我們的預期,當執行 save 保存數據後,因為出現了异常,所以事務要回滾。所以數據不會被修改。

在瀏覽器中輸入 http://localhost:8080/user/update 進行測試,注意查看日志,可以確認事務起效了。

ae3e22458f6868e5beba113dd576fd53.png

當我們把事務去掉,同樣拋出异常:

@Override
public void update(User user) {
    userRepository.save(user);
    throw new RuntimeException("啊,出現妖怪了!");
}

再次執行,發現雖然程序報錯了,但數據卻被更新了。

edb8389f22ef51cae621750ed4a152f5.png

這也間接地證明,我們的 @Transactional 事務起效了。

看到這,是不是就明白為什麼新同事的優化純屬畫蛇添足/卵用了吧?

項目源碼

  • 編程喵: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

原网站

版权声明
本文为[程序員小灰]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/172/202206210938264093.html

随机推荐