<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)化一行代碼,周三被勸退?

          共 16847字,需瀏覽 34分鐘

           ·

          2022-06-28 01:08

          點擊藍色“程序員黃小斜”關注我喲

          加個“星標”,每天和你一起多進步一點點



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

          如若轉載請聯(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ā)吧。

          謝謝支持喲 (*^__^*)

          瀏覽 57
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  成年片黄色片网站视频 | 久久综合导航 | 色婷婷视频一区二区 | 亚洲AV综合AV东京热三区 | 日本黄色一级视频 |