<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          程序員新人周一優(yōu)化一行代碼,周三被勸退?

          共 16378字,需瀏覽 33分鐘

           ·

          2022-07-01 01:52

          周一,公司新來了一個同事,面試的時候表現(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)的問題。不可重復讀是指,在一個事務內,多次讀同一數(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,其本質是對方法前后進行攔截,然后在目標方法開始之前創(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ī)則。

          事務傳播行為

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

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

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

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

          01、PROPAGATION_REQUIRED

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

          • 如果外部方法沒有開啟事務的話,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

          創(chuàng)建一個新的事務,如果當前存在事務,則把當前事務掛起。也就是說不管外部方法是否開啟事務,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()發(fā)生異常回滾,bMethod()不會跟著回滾,因為 bMethod()開啟了獨立的事務。但是,如果 bMethod()拋出了未被捕獲的異常并且這個異常滿足事務回滾規(guī)則的話,aMethod()同樣也會回滾。

          03、PROPAGATION_NESTED

          如果當前存在事務,就在當前事務內執(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í)行的最長時間,如果在超時時間內還沒有完成的話,就自動回滾。

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

          更多事務失效的場景

          測試事務是否起效

          在測試之前,我們先把 Spring Boot 默認的日志級別 info 調整為 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

          程序汪資料鏈接

          程序汪接的7個私活都在這里,經(jīng)驗整理

          Java項目分享  最新整理全集,找項目不累啦 07版

          堪稱神級的Spring Boot手冊,從基礎入門到實戰(zhàn)進階

          臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載!

          臥槽!阿里大佬總結的《圖解Java》火了,完整版PDF開放下載!

          字節(jié)跳動總結的設計模式 PDF 火了,完整版開放下載!


          歡迎添加程序汪個人微信 itwang009  進粉絲群或圍觀朋友

          瀏覽 46
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩一区二区视频 | 免费 无码 高清 | 亚洲成人中文娱乐网 | 五月丁香月| 亚洲三级黄色 |