问题过程:
问题出现于新类目导购下的商品列表页,在商品列表页选择按照某种条件手动排序时会出现排序错乱和个别商品数据重复的现象.并且此现象为随机出现.比较纳闷.
首先用Fidder进行抓包,发现是接口返回的数据跟前端展示的时一致的.确定是接口的问题,排除前端的原因,接下来进入相关接口进行查看,核心代码如下:
Map<String, Object> objMap = Maps.newHashMap();
List<String> realIdList = new ArrayList<>();
//否则就按原接口排序做
Set<String> itemIds = itemService.getItemByWebCategory(pageIndex, pageSize, orderType, categoryLevelId3);
realIdList = new ArrayList<>(itemIds);
objMap.put("itemList", itemExtendService.getItemsForList(realIdList));
objMap.put("totalCount", itemService.getItemByWebCategoryCount(categoryLevelId3));
return objMap;
里面的逻辑非常简单,就是根据前端传来的分页,排序条件和类目id去一个外部接口里查到一个商品id的Set集合,再调用另一个集合去批量查出商品数据返回给前端.由于第二个接口需要List,此处调用了List的构造方法将接口1得到的Set转为了List.
首先怀疑是接口返回的数据就是乱序的,为此telnet到相关机器上将实际出问题的类目的id和条件调用了几次,经过认真比对,发现其返回数据的排序是正确的,并且也没有出现重复商品这样的问题.初步排除了这两个RPC接口的问题.外部接口提供方也反复说不是他们的问题.就此陷入了江局.
这时经过大佬的提醒,决定回到前端抓包的结果里进行分析,将一次请求返回的结果的商品id记录下来.再进入第一个接口的具体逻辑里,发现里面的代码核心逻辑如下:
Set<String> idSet;
else if (priceup.equals(orderType)) {
idSet = redisCache.zrange(ItemConstants.ITEM_WEBCATEGORY_PRICE + categoryId, startpos, endpos);
}
发现里面是用的redis的zset集合实现,那就好办了,将接口传入的参数给到它的zrange方法里,去服务器上手动执行看下结果,一对比果然发现了不一样!也就是说接口返回给我的商品id顺序与前端抓包得到的商品id顺序不一致.所以导致了此bug.
原因分析:
set集合本身是无序的,既不保证插入顺序也不保证自然顺序.但其实现类可以保证顺序,TreeSet可以实现自然排序,LinkedHashSet则可以保证插入排序.进入jedis的zrange方法发现其内部返回就是一个linkedHashSet
但是这个坑爹的是linkedHashSet在序列化后就丢失了其内元素本身的顺序,在其源码中的序列化方法也强调了这一点:
因此这样就解释了为啥在本地调用这个接口方法时没问题,推测是本地调用时没有经过序列化与反序列化这个过程,因此本地也不会出现.而且这个丢失顺序本身也是随机的,有时候也不会丢失,故而比较隐蔽.
此外第一段代码的第二个接口的调用也有问题,即使第一个接口的返回的id是有顺序的,也不能保证第二个接口返回的商品集合就一定是按照传入的顺序来排的,除非接口的实现是对此有保证的,而且今后都可能不会修改此接口(类似于某些类线程安全保证).因此是不靠谱的.
解决方案:
1.让接口提供方修改实现或者重新开一个新接口,用List集合来返回数据, 通过查看源码文档的writeObject方法,可以看到他的各个实现都对序列化后的元素顺序进行了保证,因此很适合这种对顺序有强要求的RPC调用.
2.在第二个接口返回商品数据后,应该根据条件自己重新实现以下排序,以保证返回给前端的数据排序是正确的.
总结:
1.对常见的集合类要熟练使用,必要时进入源码里翻翻文档可以避免使用错误
2.涉及到RPC时要小心,序列化前后可能会有一些坑.
3.定位问题要找准位置,拿出实际参数或结果的记录,不然接口调用方和提供方互相甩锅,迟迟得不到解决.