美文网首页异常总结
Spring 3.1.2中的并发bug

Spring 3.1.2中的并发bug

作者: bighome | 来源:发表于2019-01-28 17:00 被阅读0次

    前些时候,在群里面看到同事抛出来的一个比较神奇的空指针异常:


    image.png

    源码背景:

    1.从代码可以确认,抛异常的点在于babyUserClientService为null
    2.其中 weddingShopBriefInfoService 和 babyUserClientService 都是单例的,且初始化和注入方式都一样。
    3.这些单例是被注入Struct2 的 Action 中
    4.Spring 版本 3.1.2.RELEASE

    告警现象:

    1.偶发-不是所有的请求都会报
    2.随机-每次报异常的点都不一样

    异常分析:

    1.这个偶发可能不是真正的偶发,而是特定场景下的必然。由于每次异常数量都比较少,而且需要过一段时间才会再次发生异常。故猜测:这个特定场景就是服务发布。查看发布记录与告警记录,结果与猜想的吻合。
    2.再结合告警现象,摆明了一副“并发问题的嘴脸”。和1中的结果想结合,应该是服务器启动后遇到并发请求时会必然出现的结果。为了验证猜想,本地起了服务,手动访问,确实无法重现问题。于是开始准备压测,将qps提升到了100,然后启动服务。果然,问题重现了。
    3.既然2中将问题重现了,说明朝着这个方向继续搞,准没错。但是究竟是什么原因呢?既然是 Spring 的 bean 注入失败,则就朝着这个方向去研究。由于 Struct2 的Action 作用域是 prototype,所以每一个请求过来Spring 都会调用 getBean 来创建Action对象的实例。实例创建成功后,Spring 会为其注入相应的依赖。目前看来是这个环节出了问题。
    4.定位到代码环节了,那肯定就不能光空想了,经过一段时间的调试,定位到相关环节的代码:

    public PropertyValues postProcessPropertyValues(
            PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
    
        InjectionMetadata metadata = findResourceMetadata(bean.getClass());
        try {
            metadata.inject(bean, beanName, pvs);
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of resource dependencies failed", ex);
        }
        return pvs;
    }
    

    不过这段代码并没什么问题,点开 findResourceMetadata 方法,这里返回了 InjectedElement 的实现 ResourceElement,再看 metadata.inject :

    public void inject(Object target, String beanName, PropertyValues pvs) throws Throwable {
        if (!this.injectedElements.isEmpty()) {
            boolean debug = logger.isDebugEnabled();
            for (InjectedElement element : this.injectedElements) {
                if (debug) {
                    logger.debug("Processing injected method of bean '" + beanName + "': " + element);
                }
                element.inject(target, beanName, pvs);
            }
        }
    }
    

    又调用 ResourceElement.inject :

    protected void inject(Object target, String requestingBeanName, PropertyValues pvs) throws Throwable {
            if (this.isField) {
                Field field = (Field) this.member;
                ReflectionUtils.makeAccessible(field);
                field.set(target, getResourceToInject(target, requestingBeanName));
            }
            else {
                if (checkPropertySkipping(pvs)) {
                    return;
                }
                try {
                    Method method = (Method) this.member;
                    ReflectionUtils.makeAccessible(method);
                    method.invoke(target, getResourceToInject(target, requestingBeanName));
                }
                catch (InvocationTargetException ex) {
                    throw ex.getTargetException();
                }
            }
        }
    

    可以看到,如果注解在字段上的话,是通过 ResourceElement.getResourceToInject 来获取依赖对象的:

            @Override
        protected Object getResourceToInject(Object target, String requestingBeanName) {
            Object value = null;
            if (this.cached && this.shareable) {
                value = this.cachedFieldValue;
            }
            synchronized (this) {
                if (!this.cached) {
                    value = getResource(this, requestingBeanName);
                    if (value != null && this.shareable) {
                        this.cachedFieldValue = value;
                        this.cached = true;
                    }
                }
            }
            return value;
        }
    

    重点来了,可以看出以上代码有个很明显的并发问题。可以想象,几个线程同时进入到 synchronized 这块,一个线程获得锁,进去了。执行完成后就将 this.cached = true,然后释放锁。而那个和他一起到 synchronized 的代码,在获取锁后,都会跳过 if(!this.cached ),而此时的 value 依旧等于null。这边还看了其他注解处理器:@Autowired、@Inject 等都有一样的问题

    至此,以上所有的现象都能说通了。

    结论

    所有延迟创建的bean都可能存在并发问题(当然,这个问题只能是存在这个版本)

    解决问题

    问题既然已经定位到了,就要想怎么解决。由于是 Spring 的问题,可以分为2种方案:

    1.spring 发现且在高版本中已经解决这个问题。那可以通过升级版本来解决。

    2.spring 没有解决这个问题,则一边通过向spring 提 bug,一边先用服务预热的方式解决(启动过程中先调用getbean)。

    结果也如想象的一样,spring 在 高版本中已经解决了这个问题,这里选择3.2.13.RELEASE版本查看代码,发现其已经不再使用锁机制获取对象了。具体逻辑这里不再累赘。有兴趣的小伙伴可以自行查看源码。

    由于 3.2.13.RELEASE 和 3.1.2.RELEASE 都是 3.x版本下,差别不是太大。升级风险比较小。所以推荐可以直接升级至 3.2.13.RELEASE 。

    我这里升级版本至 3.2.13.RELEASE 后放置 beta服务启动后再进行压测,发现已经没有 空指针异常了。

    让相关同学升级后反馈问题解决。

    相关文章

      网友评论

        本文标题:Spring 3.1.2中的并发bug

        本文链接:https://www.haomeiwen.com/subject/dsgajqtx.html