记得之前有次发布后线上莫名偶发性出现 NPE,出现 NPE 还好,问题在于 NPE 的地方特别奇怪:
当时看到这个 Exception 一脸懵,这算啥问题?先来尝试直接来看报错点:
// MimeHeaders
private void findNext() {
next=null;
for(; pos< size; pos++ ) {
next=headers.getName( pos ).toString(); // NPE
for( int j=0; j<pos ; j++ ) {
if( headers.getName( j ).equalsIgnoreCase( next )) {
// duplicate.
next=null;
break;
}
}
if( next!=null ) {
// it's not a duplicate
break;
}
}
// next time findNext is called it will try the
// next element
pos++;
}
public MessageBytes getName(int n) {
return n >= 0 && n < count ? headers[n].getName() : null;
}
结合上述两段代码来看,最可疑的是 getName 返回 null 从而导致 toString 调用触发 NPE!那什么时候回返回 null?考虑到 n >= 0 基本上为 true ( 不然就是代码 bug ),那么自然剩下的 n < count,观察到在另外一个方法中有这么一个逻辑:
public void clear() {
for (int i = 0; i < count; i++) {
headers[i].recycle();
}
count = 0;
}
通过断点发现该方法会在一次请求后调用,那么看起来问题就很清晰了!想到问题是在上一次发版之后才出现,直接看修改记录,发现有如下代码 ( 为方便理解移除无关代码 ):
public long exportItemInformation() {
// ······
return baseTaskService.asyncExportItemInformation(RequestContextHolder.getRequestAttributes());
}
@Async("asyncTaskExecutor")
public void asyncExportItemInformation(RequestAttributes requestAttributes) {
RequestContextHolder.setRequestAttributes(requestAttributes);
// ······
}
看到这两段代码段,顿时也就明白问题所在 -- 在主方法请求完成后,其中的 MimeHeaders 会被 clear,而异步方法则在使用。至此,问题的根因浮现!( 自然解决办法也相当简单,相信聪明如你立马能想到 )
并发问题相当难定位,在设计上能不用就不用,这也是为什么 Redis 坚持单线程执行 command 的原因。