1. 程式人生 > >小林求職記(二):說好的問基礎,為啥我感覺一點也不基礎呢?

小林求職記(二):說好的問基礎,為啥我感覺一點也不基礎呢?

在上一輪的面試中,小林在mysql方面因為作答不夠完善,被面試官吊打了一番。經過兩天的自我複習之後,新的一輪面試又開始了。

面試官:你好,請簡單介紹下自己吧。

小林:你好,我是xxxxxx,之前在深圳的xxx公司負責了xxx系統的研發設計。

面試官:嗯嗯,那我先來問你一些基礎問題吧。

小林:嗯嗯,好的。

面試官:你瞭解arraylist吧,請說下內部的一些特性。

小林此時心裡一下子樂開了花,這個簡單啊。

小林:arraylist的底層主要是由陣列組成,它和普通陣列不太一樣,arraylist具有自動擴容的功能。每次當我們add一個元素到佇列裡面的時候,都會有一步確認容量的機制判斷(對應原始碼裡面的ensureCapacityInternal函式)如果當陣列內部的元素達到了陣列閾值的時候,就會以1.5倍的體積去做擴容,底層是呼叫了才做系統內部的一個System.arraycopy方法。

又由於arraylist是採用陣列儲存的,在讀取資料的時候可以藉助陣列位的下標去快速定位,寫資料的時候需要涉及到挪動陣列,所以讀的效能平均要比寫的效能更高一些。

面試官:嗯嗯,回答地挺全面的。那你覺得在使用arraylist的時候一般會注意些什麼嗎?

小林:嗯嗯,有的。一般我會根據程式碼的上下文給arraylist附一個初始值來定位這個陣列的大小,防止其做過多不必要的擴容操作。另外在迴圈中進行刪除操作的時候需要注意會有坑,一般建議採用迭代器的模式來處理。

ps:

如果使用以下這種方式進行元素的移除可能會導致出現刪除元素不完整的情況:

public static void main(String[] args) {
    ArrayListApplication arrayListApplication = new ArrayListApplication();
    List<String> list = new ArrayList(3);
    list.add("a");
    list.add("c");
    list.add("c");
    list.add("d");
    list.add("e");
    System.out.println(list);
    System.out.println("==========");
    removeV1(list, "c");
    System.out.println(list);
}
public static void removeV1(List<String> list, String deleteItem) {
    for (int i = 0; i < list.size(); i++) {
        String item = list.get(i);
        if (item.equals(deleteItem)) {
            list.remove(item);
        }
    }
}

 

列印結果:

[a, c, c, d, e]
==========
[a, c, d, e]

 

此時由於刪除掉list裡面元素之後,list的size值也減少了,隨之導致了陣列元素的前移,因此會出現被刪除元素的後一位直接繞開了if判斷,沒有被“命中”。如果採用foreach的方式刪除,則會丟擲一段異常資訊,宣告刪除失敗:

Exception in thread "main" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
 at java.util.ArrayList$Itr.next(ArrayList.java:859)

 

面試官:嗯嗯,你剛剛有說到迭代器刪除,那麼你有了解過迭代器模式嗎?

小林:額,迭代器模式,讓我思考一下.... 背了知識點,但是不記得了....

面試官:好吧...

ps:其實迭代器模式的好處在於,直接幫我們遮蔽了具體的實現細節,通過採用迭代器的方式來幫助我們編寫一些可以複用的類。例如說arraylist,其實內部也有自己迭代器的具體實現,vector也有自己迭代器模式的具體實現。非常的方便,並且有助於減少程式碼裡面的對於具體實現的強依賴性。

面試官:那我們切入下一個話題吧,能說下自己對於幻讀的理解嗎?

小林:嗯嗯,可以的。我在上家公司工作的時候,公司內部的事務隔離級別設定為了可重複讀級別,這樣能夠保證當前事務讀取的資料不會受到其他事務提交的影響,但是這種隔離級別會在事務提交完畢之後查詢資料的時候出現幻讀的場景,如果需要解決幻讀的情況需要將事務的隔離級別提升為序列化等級。

面試官:哦,那你在工作中有試過提升為序列化嗎?

小林:沒有,因為序列化是強行在mysql層加鎖,使得事務得排隊執行,容易產生堵塞的情況,效能不佳。

面試官:那你是怎麼解決幻讀的情況呢?

小林:嗯....別的同事幫我解決的.....

此時,面試官臉上漸漸露出了詭異的笑容。

ps:其實幻讀這種情況在工作中偶爾還是會遇到的,舉個具體場景:

假設某一時刻,同時有兩個事務訪問了資料庫,需要先從庫裡面查詢訂單是否存在,然後再插入新的訂單記錄。

a連線

mysql> select @@global.tx_isolation,@@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 2 warnings (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_order_1 where id =100;
Empty set (0.00 sec)

 

b連線

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_order_01 where id =100;
ERROR 1146 (42S02): Table 'test-db01.t_order_01' doesn't exist
mysql> select * from t_order_1 where id =100;
Empty set (0.00 sec)

 

假設在兩邊事務都開啟的一刻,a連線中的事務往資料庫插入了一條id為100的資料,然後commit。a連線

mysql> INSERT INTO `test-db01`.`t_order_1` ( `id`, `order_no`, `product_id`, `user_id`, `create_time`, `update_time` )
    -> VALUES
    -> ( 100, 2, 2, 2, now(), now());
Query OK, 1 row affected (0.00 sec)
mysql> select * from t_order_1 where id=100;
+-----+----------+------------+---------+---------------------+---------------------+
| id  | order_no | product_id | user_id | create_time         | update_time         |
+-----+----------+------------+---------+---------------------+---------------------+
| 100 |        2 |          2 |       2 | 2020-07-14 22:57:37 | 2020-07-14 22:57:37 |
+-----+----------+------------+---------+---------------------+---------------------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

 

當a事務提交結束了,此時b事務開始執行select 查詢校驗的操作,判斷不存在id為100的資料,此時打算執行插入資料的操作:

mysql> select * from t_order_1 where id=100;
Empty set (0.00 sec)
mysql> INSERT INTO `test-db01`.`t_order_1` ( `id`, `order_no`, `product_id`, `user_id`, `create_time`, `update_time` )
    -> VALUES
    -> ( 100, 2, 2, 2, now(), now());
ERROR 1062 (23000): Duplicate entry '100' for key 'PRIMARY'

 

結果出現了異常,這種情況我們通常稱之為幻讀。那麼該如何解決這種場景的問題呢?其實mysql內部提供了一種叫做next-key-lock的加鎖機制,可以供我們處理這類特殊情況:

mysql> select * from t_order_1 where id=100 for update;
+-----+----------+------------+---------+---------------------+---------------------+
| id  | order_no | product_id | user_id | create_time         | update_time         |
+-----+----------+------------+---------+---------------------+---------------------+
| 100 |        2 |          2 |       2 | 2020-07-14 23:06:03 | 2020-07-14 23:06:03 |
+-----+----------+------------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

 

藉助 **for update **語句,我們可以在應用程式的層面手工實現資料加鎖保護操作。就是那些需要業務層面資料獨佔時,可以考慮使用** for update**。

其實 for update 可以理解為一把悲觀鎖,每次獲取資料的時候,都擔心會有其他執行緒修改當前的資料,因此在拿資料的時候就會加入一把鎖,其他試圖改寫資料的請求將會處於堵塞情況。(讀資料的請求不會堵塞)

面試官:那你知道mysql裡面的mvcc機制嗎?

小林:mvcc是啥?mvc我倒知道,之前工作中有使用過springmvc框架 blabla(希望把面試官繞開引到自己熟系的話題方向)

面試官滿臉微笑地看著小林,似乎想緩解下尷尬的氣氛。

面試官:好吧,你之前有對快取瞭解過嗎?

小林:嗯嗯,我在工作中一般喜歡使用redis作為快取。當查詢資料的時候先去redis中查詢,如果redis沒有再去mysql中讀取資料。

面試官:嗯嗯,那你有了解過redis裡面的哪些資料結構嗎?

小林:嗯嗯,我在工作中有使用過string,list,hash,zset,set這幾類資料結構,它們各自都有自己的特點,在使用的時候需要結合實際的業務場景來使用。

String型別可以用於儲存一些簡單的鍵值對資料,例如數字,字串之類的。

List結構一般是採用了雙端佇列的結構,這類結構通常使用的命令有lpush,lpop,rpop等,如果想移除某個節點的前置和後置節點就比較簡單(複雜度就是O(1)),但是搜尋比較複雜。適合用於儲存一些列表型別的資料資訊,例如說使用者的留言和評論資訊。

Set 是一個無順序的集合,比較常見的例如說交集查詢,用於搜尋兩個好友之間共同閱讀過的圖書。或者兩個人之間的共同好友等。使用命令sinter key [key...] 即可實現。

Hash是包含鍵值對的無序散列表,常用命令有:hget,hgetall等。

ZSet則是一套有序集合,通常會根據分值範圍或者成員來獲取元素並且計算一個鍵的排名。

面試官:嗯嗯,你講的這些東西都只是停留在了表層,關於其內部的構造有去做過一些深入的瞭解嗎?

小林:額,沒有....

此時小林又一次被面試官打擊了自信心

面試官:好吧,今天的一面主要只是問問基礎,那麼就先這樣吧,我去找下我老大詢問下。

小林:嗯嗯,好的。(似乎感覺還有戲)

小林一個人在前臺坐著等待著下一場面試的到來,不僅內心感嘆道,自己過去的工作中過多地安逸於寫crud,很多java的基礎問題也都記得不太清楚了,每天下班之後也沒怎麼學習,雖然一面只是問了些基礎問題,結果卻暴露了自己這麼多的知識盲區。

過了不久一個陌生的男人慢慢走了過來,天啊,這傢伙真的是聰明“絕頂” 了。小林一下子慌了,腦袋一片空白,二面似乎來了一位資深的大佬.....

二面面試官:你好,我是你的二面面試官,下邊我可能會針對你的專案做一些詢問。

(未完待續...