1. 程式人生 > 其它 >線上問題引發的對於Map.values()的探究思考

線上問題引發的對於Map.values()的探究思考

技術標籤:線上問題排查Javajavahashmap

專案場景:

專案場景:我們的資料需要同步到es,以供列表頁查詢。產品反映修改了資料之後,列表頁顯示的欄位可能是修改之前的甚至更老版本的資料。

問題描述:

我們目前做的一個系統,表單資料會有多個版本,但是不同版本欄位不一定相同。需要把所有資料同步到es,在es進行列表搜尋顯示。上線幾天後發現一個詭異的事兒是,某些表單,在經過多次編輯之後,從es查到的列表資料某些欄位會展示位舊資料,而表單詳情是最新資料。

  1. 定位問題,問題一定出現在表單多版本資料融合,然後同步到es的地方
  2. 檢視程式碼
	 		List<String> taskDetailCodes =
taskEntities.stream() .sorted(Comparator.comparing(BaseEntity::getCreateTime)) .map(TicketTaskEntity::getDetailCode) .filter(StringUtils::isNotBlank) .collect(Collectors.toList()); Map<String, Object> detail =
new HashMap<>(); taskDetailCodes.add(0, ticketEntity.getDetailCode()); // 工單主表單資料處理 Map<String, Map<String, String>> taskDetailMap = objectDAO.getMapByCodes(taskDetailCodes); detail = combineMap(taskDetailMap.values());

下面是combineMap的邏輯

private Map<
String, Object> combineMap(Collection<Map<String, String>> detailList) { Map<String, Object> combinedMap = new HashMap<>(); for (Map<String, String> map : detailList) { for (Map.Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); if (BuiltInFieldEnum.ASSIGNEE.name().equals(key)) { // 受理人,儲存所有 ......... } else { // 其他情況,只保留最新的值 addToDetail(combinedMap, entry); } } } return combinedMap; }

綜上,可以看到具體邏輯是,先查出表單資料的各個版本,然後利用createTime排序,把多版本的資料按照先後順序放入到detail中,同時相同欄位進行覆蓋,這樣某個欄位最新的value,一定是最後儲存到detail裡面的那個。

原因分析:

  1. 通過排查,發現出問題的表單查出來的最終ticketDetailCodes的資料是,其中欄位為desc
	String key1 = "1AN1GKELGK3PPSEC";// desc: test1
    String key2 = "1AN1GKELGK3PQ51G";// desc: test0
    String key3 = "1AO1GKIO2DCTF4EC";// desc: test1

按照線上問題,模擬出上述場景,三個key對應的map中都有desc欄位,value分別在註釋處,按照業務邏輯,應該是key3的desc最終覆蓋之前的desc,所以整個detail的desc的值應該是test1,但是實際場景確實desc的值為test0。
大腦靈光一閃,detail = combineMap(taskDetailMap.values());,這句程式碼返回的value也許根本沒有按照我們put進map的順序,然後做了個實現:

	public static void main(String[] args) {
        String key1 = "1AN1GKELGK3PPSEC";  // hash = "-1104869755",  (n - 1) & hash = 5
        String key2 = "1AN1GKELGK3PQ51G";  // hash = "-1104869412", (n - 1) & hash = 12
        String key3 = "1AO1GKIO2DCTF4EC";  // hash = "1370718008", (n - 1) & hash = 8
        Map<String,String> map = new HashMap<>();
        map.put(key1, key1);
        map.put(key2, key2);
        map.put(key3, key3);
        System.out.println(map.values());
    }

輸出

[1AN1GKELGK3PPSEC, 1AO1GKIO2DCTF4EC, 1AN1GKELGK3PQ51G]

可以看到,確實和我們設想的一致,打印出來的順序1AN1GKELGK3PQ51G反而到了最後一個,所以最終detail裡的desc的值用的也就是它的,也就是test0

HashMap.values()的返回值

因為咱們用的是HashMap,所以來看看hashmap中的原始碼

public Collection<V> values() {
        Collection<V> vs = values;
        if (vs == null) {
            vs = new Values();
            values = vs;
        }
        return vs;
    }
final class Values extends AbstractCollection<V> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<V> iterator()     { return new ValueIterator(); }
        public final boolean contains(Object o) { return containsValue(o); }
        public final Spliterator<V> spliterator() {
            return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super V> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.value);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

根據原始碼可以發現,最終返回的邏輯就是Values類裡forEach的邏輯,也就是把HashMap裡的值,遍歷tab屬性的下標,然後遍歷每個下標下的連結串列。此處就不累贅HashMap的putVal方法了,但是可以把我的實驗結果列出來:

		String key1 = "1AN1GKELGK3PPSEC";  
        String key2 = "1AN1GKELGK3PQ51G";  
        String key3 = "1AO1GKIO2DCTF4EC";  
        Map<String,String> map = new HashMap<>();
        map.put(key1, key1); // hash = "-1104869755",  (n - 1) & hash = 5
        map.put(key2, key2); // hash = "-1104869412", (n - 1) & hash = 12
        map.put(key3, key3); // hash = "1370718008", (n - 1) & hash = 8
        System.out.println(map.values());

由於這3個key都沒有衝突,所以他們分別放到了如下的下標裡key1->5,key2->12, key3->8,這樣的話,如果呼叫values(),那麼返回的就會是按照key1,key3,key2的順序返回value,但是因為不是必現,所以測試階段測試同學沒有測出來。
那麼如果用LinkedHashMap呢?

LinkedHashMap.values()

public Collection<V> values() {
        Collection<V> vs = values;
        if (vs == null) {
            vs = new LinkedValues();
            values = vs;
        }
        return vs;
    }
final class LinkedValues extends AbstractCollection<V> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        public final Iterator<V> iterator() {
            return new LinkedValueIterator();
        }
        public final boolean contains(Object o) { return containsValue(o); }
        public final Spliterator<V> spliterator() {
            return Spliterators.spliterator(this, Spliterator.SIZED |
                                            Spliterator.ORDERED);
        }
        public final void forEach(Consumer<? super V> action) {
            if (action == null)
                throw new NullPointerException();
            int mc = modCount;
            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                action.accept(e.value);
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

可以看到LinkedHashMap返回順序是按照內部定義的雙向連結串列的node順序返回的,因為LinkedHashMap雙向列表的順序可以通過accessOrder控制是按照插入順序還是修改順序排序,但是預設是按照插入順序排序。
做個簡單的實驗

	public static void main(String[] args) {
        String key1 = "1AN1GKELGK3PPSEC";
        String key2 = "1AN1GKELGK3PQ51G";
        String key3 = "1AO1GKIO2DCTF4EC";
        Map<String,String> map = new LinkedHashMap<>();
        map.put(key1, key1);
        map.put(key2, key2);
        map.put(key3, key3);
        System.out.println(map.values());
    }

以下是輸出

[1AN1GKELGK3PPSEC, 1AN1GKELGK3PQ51G, 1AO1GKIO2DCTF4EC]

可以看到輸出的順序和插入順序一致

解決方案:

既然原因找到了,其實可以按照以上的分析,提供兩種解決方案:

  1. 利用LinkedHashMap的特性來解決:修改objectDAO.getMapByCodes(taskDetailCodes);返回的map的型別為LinkedHashMap,但是因為可能影響其他呼叫處,未使用該方法
	Map<String, Map<String, String>> taskDetailMap = objectDAO.getMapByCodes(taskDetailCodes);

2.根據排序完的taskDetailCodes的順序,重新組裝list,而不直接使用taskDetailMap.values()。這樣程式碼的可讀性也比較強,雖然程式碼不夠優雅。最終程式碼如下:

	Map<String, Map<String, String>> taskDetailMap = objectDAO.getMapByCodes(taskDetailCodes);
    List<Map<String, String>> taskDetailMapList = Lists.newLinkedList();
    for (String taskDetailCode : taskDetailCodes) {
        taskDetailMapList.add(taskDetailMap.getOrDefault(taskDetailCode, Collections.emptyMap()));
    }
    detail = combineMap(taskDetailMapList);