线上问题引发的对于Map.values()的探究思考

项目背景:

项目北京:我们的数据需要同步到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);
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值