解決一次i++引發的bug

2020-10-19 21:00:53

欄目為大家介紹i++引發的bug。

大家好,作為日常寫bug修bug的我,今天給大家帶來前幾天剛剛修復的一個事故。不得不承認,有我的地方總是會有這麼多bug。

起因

故事的開始發生在前幾天,有一個不是很常用的匯出功能,被使用者反饋出,不管條件是怎麼樣,匯出的資料只有一條,但是實際上根據條件查詢是有很多資料,而且頁面中也查詢出很多資料。(這個問題已經被修復了,所以當時的Kibana紀錄檔也找不到了)於是放下手上的工作,投入其中來看這個問題。

分析

從問題的描述中來分析,那麼只可能出現在以下情況:

  1. 根據搜尋條件查詢出來的記錄只有一條。
  2. 對查詢出來的資料進行相關業務處理,導致最後的結果只有一條。
  3. 檔案匯出元件的邏輯處理之後,導致結果只有一條。

題外話
寫到了這裡,突然想到了一個經典面試題,MQ訊息丟失的場景原因分析。哈哈哈,其實大致上也是這麼幾個角度分析。(有機會來寫MQ的文章)
題外話

於是就一個個來分析:

  1. 找到相關業務的SQL以及對應的引數,查詢可得,資料不止1條,所以第一種情況可以排除。
  2. 中間業務當中有涉及到相關許可權、資料敏感等,將這些都放開之後,還是隻有1條資料。
  3. 檔案匯出元件在接收到資料的時候,列印出的紀錄檔也顯示只有一條,那麼可以說明肯定中間相關業務的邏輯發生了問題。

由於這段程式碼都是寫在一整個方法裡面,導致Arthas排查起來就比較困難,只好一步步設定紀錄檔進行排查。(所以,如果是一大段邏輯,建議是拆分成duoge 子方法,一來在寫的時候思路明確,有一種模組化的概念,至於方法複用什麼的我就不多提了,基本操作;二是一但發生問題,排查起來也會方便點,經驗之談)。
最終定位到一個for迴圈裡面。

程式碼

話不多說,我們直接來看程式碼。眾所周知,我向來是一個很保護公司程式碼的人,所以,我在這裡又不得不給大家模擬一下了。從問題的情況來看,是匯出的物件記錄是空

import com.google.common.collect.Lists;import java.util.List;public class Test {    public static void main(String[] args) {        // 獲取Customer資料,這裡就簡單模擬
        List<Customer> customerList = Lists.newArrayList(new Customer("Java"), new Customer("Showyool"), new Customer("Soga"));        int index = 0;
        String[][] exportData = new String[customerList.size()][2];        for (Customer customer : customerList) {
            exportData[index][0] = String.valueOf(index);
            exportData[index][1] = customer.getName();
            index = index++;
        }
        System.out.println(JSON.toJSONString(exportData));
    }
}class Customer {    public Customer(String name) {        this.name = name;
    }    private String name;    public String getName() {        return name;
    }    public void setName(String name) {        this.name = name;
    }
}複製程式碼

這段程式碼看起來好像也沒什麼的,就是將Customer集合轉換成一個字串二維陣列。但是輸出結果顯示:這就符合我們說的,查詢出來有多條,但是輸出只有1條。
仔細觀察一下,我們可以發現,輸出的資料顯示都是最後一條,也就是說,Customer這個集合每次遍歷的時候,都是後者將前者進行覆蓋,也就是說,這個index的下標一直沒有變化過,一直是0。

建模

這樣看來,我們的這個自增確實有點問題,那麼我們再簡單來寫一個模型

public class Test2 {    public static void main(String[] args) {        int index = 3;
        index = index++;
        System.out.println(index);
    }
    
}複製程式碼

我們將上面的業務邏輯簡化成這樣一個模型,那麼這個結果毫無意外的是3。

解釋

那麼我們執行一下javap,看看JVM位元組碼是如何解釋:

javap -c Test2

Compiled from "Test2.java"public class com.showyool.blog_4.Test2 {  public com.showyool.blog_4.Test2();
    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:       0: iconst_3       1: istore_1       2: iload_1       3: iinc          1, 1
       6: istore_1       7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: iload_1      11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      14: return}複製程式碼

這裡我簡單講一下這裡的JVM位元組碼指令(後面有機會再詳細寫寫文章)
首先我們需要先知道這裡存在兩個概念,運算元棧和區域性變數表,這兩者是存在虛擬機器器棧當中棧幀(stack frame)當中的一些資料結構,如圖:

我們可以簡單的理解為,運算元棧的作用是存放資料並且在棧中進行計算資料,而區域性變數表則是存放變數的一些資訊。
然後我們來看看上面的指令:
0: iconst_3 (先將常數3壓入棧)

1: istore_1 (出棧操作,將值賦給第一個引數,也就是將3賦值給index)

2: iload_1 (將第一個引數的值壓入棧,也就是將3入棧,此時棧頂的值為3)

3: iinc 1, 1 (將第一個引數的值進行自增操作,那麼此時index的值是4)

6: istore_1 (出棧操作,將值賦給第一個引數,也就是將3賦值給index)

也就是說index這個引數的值是經歷了index->3->4->3,所以這樣一輪操作之後,index又回到了一開始賦值的值。

延伸一下

這樣一來,我們發現,問題其實出在最後一步,在進行運算之後,又將原先棧中記錄的值重新賦給變數,覆蓋掉了 如果我們這樣寫:

public class Test2 {    public static void main(String[] args) {        int index = 3;
        index++;
        System.out.println(index);
    }

}

Compiled from "Test2.java"public class com.showyool.blog_4.Test2 {  public com.showyool.blog_4.Test2();
    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:       0: iconst_3       1: istore_1       2: iinc          1, 1
       5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: iload_1       9: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      12: return}複製程式碼

可以發現,這裡就沒有最後一步的istore_1,那麼在iinc之後,index的值就變成我們預想的4。
還有一種情況,我們來看看:

public class Test2 {    public static void main(String[] args) {        int index = 3;
        index = index + 2;
        System.out.println(index);
    }

}

Compiled from "Test2.java"public class com.showyool.blog_4.Test2 {  public com.showyool.blog_4.Test2();
    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:       0: iconst_3       1: istore_1       2: iload_1       3: iconst_2       4: iadd       5: istore_1       6: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: iload_1      10: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      13: return}複製程式碼

0: iconst_3 (先將常數3壓入棧)
1: istore_1 (出棧操作,將值賦給第一個引數,也就是將3賦值給index)
2: iload_1 (將第一個引數的值壓入棧,也就是將3入棧,此時棧頂的值為3)
3: iconst_2 (將常數2壓入棧, 此時棧頂的值為2,2在3之上)
4: iadd (將棧頂的兩個數進行相加,並將結果壓入棧。2+3=5,此時棧頂的值為5)
5: istore_1 (出棧操作,將值賦給第一個引數,也就是將5賦值給index)

看到這裡各位觀眾老爺肯定會有這麼一個疑惑,為什麼這裡的iadd加法操作之後,會影響棧裡面的資料,而先前說的iinc不是在棧裡面操作?好的吧,我們可以看看JVM虛擬機器器規範當中,它是這麼描述的:

指令iinc對給定的區域性變數做自增操作,這條指令是少數幾個執行過程中完全不修改運算元棧的指令。它接收兩個運算元: 第1個區域性變數表的位置,第2個位累加數。比如常見的i++,就會產生這條指令

看到這裡,我們知道,對於一般的加法操作之後複製沒啥問題,但是使用i++之後,那麼此時棧頂的數還是之前的舊值,如果此刻進行賦值就會回到原來的舊值,因為它並沒有修改棧裡面的資料。所以先前那個bug,只需要進行自增不賦值就可以修復了。

最後

感謝各位能夠看到這裡,以上就是我處理這個bug的全部過程。雖然這只是一個小bug,但是這一個小小的bug還是值得學習和思考的。今後還會繼續分享我所發現的bug以及知識點,如果我的文章對你有所幫助,還希望各位大佬點個關注\color{red}{點個關注}點個贊\color{red}{點個贊},再次感謝大家的支援!

相關免費學習推薦:

以上就是解決一次i++引發的bug的詳細內容,更多請關注TW511.COM其它相關文章!