【线上问题】记一次公司日志基础组件SPI使用不当导致业务中断

本文主要是介绍【线上问题】记一次公司日志基础组件SPI使用不当导致业务中断,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Informal Essay By English

It is always a pleasure to learn

背景

叮叮叮、叮叮叮…,某年某月某日晚上,上海某出租屋内,刚被放在桌上的手机的铃声在安静的屋内显得很piercing。来电显示是一个广东电话号码,电话号码非常的熟悉,是系统的告警专用电话。我平静的打开电脑,打开钉钉,看了一下alert群内的异常信息。然后开始熟练的打开公司的日志平台,进行异常聚合搜索。嗯~,很好,有很多的异常,看来有的看了。然后15分钟后,不出意料的找到了异常的根因,这次告警有好几处异常,本文只分析、描述跟业务无关的异常。

问题描述

当时在日志平台上输出的异常如下:
在这里插入图片描述
由于完整的日志输出涉及到公司的代码, 这里只截图部分关键堆栈信息。抛出异常的类是属于基建日志组件包,贴一下异常抛出点的代码:

public class Operators {static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);public static Object current() {//dosomethingfor (OperatorGetter i : OperatorGetter) {Object operator = i.currentOperator();if (operator != null) {return operator;}}return null;}}

问题分析

问题出现在前端调用一个后端业务接口没有成功。在用户层面的来看,表现为用户触发一次业务请求没有成功。

java.util.NoSuchElementException 是 Java 编程语言中的一个异常类,属于 java.util 包。这个异常通常在试图访问一个枚举(Enumeration)、迭代器(Iterator)或者其他类型的集合中的元素,但已经没有更多的元素时抛出。

当时看到这个异常一开始以为是META-INF/services/下面没有定义相关接口文件,但是后面通过分析拉到的jar,发现里面有相应的接口定义文件与实现。到这里已经先排除SPI没有找到对应的实现类而抛出异常的场景。到这一步SPI的错误的使用方式场景我们已经排除,接下来就只能从SPI的实现角度去分析这个问题。SPI这个知识点博主在之前的文章中已经有了详细的介绍,感兴趣的可以去看SPI详解 ,但是为了使文章能够顺畅的阅读下去,这里还是对SPI最核心的一些实现进行简单的描述。

SPI

Java的SPI(Service Provider Interface)是一种服务发现机制。它允许服务提供者在运行时被发现和加载,而不是在编译时硬编码。SPI是一种为某些接口寻找服务实现的方式,是Java提供的一种原生的插件功能。它主要用于可以插拔的组件之间的解耦。

在Java的SPI机制中,服务提供者会在类路径下的 META-INF/services 目录中创建一个名字为服务接口全限定名的文件。该文件内部列出了实现该服务接口的具体实现类的全限定名。在运行时,Java的SPI机制会查找这些配置文件,并加载并实例化这些实现类,从而实现了服务的动态查找与加载。

Java的SPI广泛应用于JDK中,例如java.sql.Driver 接口,JDBC驱动就是通过SPI机制被加载的。应用程序可以通过 ServiceLoader 类来加载服务:

ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {// 使用service
}

这里,MyService 是服务接口,而具体的实现类可以在运行时通过放置在 META-INF/services 目录下的配置文件来指定。

SPI的基本介绍完成,我们再来看看SPI的核心api的实现。

java.util.ServiceLoader#load(java.lang.Class)
public static <S> ServiceLoader<S> load(Class<S> service) {//获取应用类加载器ClassLoader cl = Thread.currentThread().getContextClassLoader();//调用了另一个load方法进行ServiceLoader对象的创建return ServiceLoader.load(service, cl);}public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){return new ServiceLoader<>(service, loader);}

load方法完成ServiceLoader对象的创建,其中需要我们关注的是在ServiceLoader构造器的中会调用一个reload方法,此方法会进行迭代器类的创建,此类是SPI最核心的实现类。

private ServiceLoader(Class<S> svc, ClassLoader cl) {service = Objects.requireNonNull(svc, "Service interface cannot be null");loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;reload();}public void reload() {providers.clear();//在此处进行懒加载迭代器类对象的创建lookupIterator = new LazyIterator(service, loader);}
java.util.ServiceLoader.LazyIterator#hasNext
public boolean hasNext() {if (acc == null) {return hasNextService();} else {PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc);}}
private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {//这里的PREFIX就是META-INF/services/String fullName = PREFIX + service.getName();if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}pending = parse(service, configs.nextElement());}nextName = pending.next();return true;}

本文不对hasNextService()方法里面的各种处理去做详细的分析,但是有一个点需要我们知道的是,这个方法没有进行并发场景下的处理。

java.util.ServiceLoader.LazyIterator#next
public S next() {if (acc == null) {return nextService();} else {PrivilegedAction<S> action = new PrivilegedAction<S>() {public S run() { return nextService(); }};return AccessController.doPrivileged(action, acc);}}private S nextService() {//这里的NoSuchElementException~~~~大家自己想象⛄️if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {c = Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {fail(service,"Provider " + cn + " not found");}if (!service.isAssignableFrom(c)) {fail(service,"Provider " + cn  + " not a subtype");}try {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();          // This cannot happen}

这个方法就是代码案例获取实例对象最终会调用的方法,这里的if (!hasNextService())throw new NoSuchElementException();对于后面分析问题很重要~

至此,SPI的使用与实现我们都有大概的了解。这里再针对SPI的并发问题做一个解释,SPI本身的概念并不直接涉及线程安全问题。线程安全主要取决于SPI的具体实现。也就是说,一个服务提供者实现的线程安全性是由提供该服务的类或者库的作者来保证的。

到这里大家其实都已经知道这次的异常是什么原因导致。那我们就直接开始问题处理

问题处理

处理方式一:
通过加锁进行处理,加锁又有synchronized、juc lock两种方式,下面贴下两种处理方式代码:

public class Operators {static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);static ReentrantLock lock = new ReentrantLock();static Object monitor = new Object();public static Object current() {CallContext context = CallContexts.get();if (context != null) {return context.getOperator();}lock.lock();try {for (OperatorGetter i : OperatorGetter) {Object operator = i.currentOperator();if (operator != null) {return operator;}}} finally {lock.unlock();}synchronized (monitor){for (OperatorGetter i : OperatorGetter) {Object operator = i.currentOperator();if (operator != null) {lock.unlock();return operator;}}}return null;}
}

处理方式二:
static方法块保证线程安全,代码如下:

public class Operators {static ServiceLoader<OperatorGetter> OperatorGetter = ServiceLoader.load(OperatorGetter.class);static Object operator;static {for (OperatorGetter i : OperatorGetter) {Object object = i.currentOperator();if (operator != null) {operator = object;}}}public static Object current() {CallContext context = CallContexts.get();if (context != null) {return context.getOperator();}for (OperatorGetter i : OperatorGetter) {Object operator = i.currentOperator();if (operator != null) {return operator;}}return null;}
}

最后提出一个问题,如果是你碰到这个问题,你会怎么去处理呢?

这篇关于【线上问题】记一次公司日志基础组件SPI使用不当导致业务中断的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/819620

相关文章

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

详谈redis跟数据库的数据同步问题

《详谈redis跟数据库的数据同步问题》文章讨论了在Redis和数据库数据一致性问题上的解决方案,主要比较了先更新Redis缓存再更新数据库和先更新数据库再更新Redis缓存两种方案,文章指出,删除R... 目录一、Redis 数据库数据一致性的解决方案1.1、更新Redis缓存、删除Redis缓存的区别二

oracle数据库索引失效的问题及解决

《oracle数据库索引失效的问题及解决》本文总结了在Oracle数据库中索引失效的一些常见场景,包括使用isnull、isnotnull、!=、、、函数处理、like前置%查询以及范围索引和等值索引... 目录oracle数据库索引失效问题场景环境索引失效情况及验证结论一结论二结论三结论四结论五总结ora

element-ui下拉输入框+resetFields无法回显的问题解决

《element-ui下拉输入框+resetFields无法回显的问题解决》本文主要介绍了在使用ElementUI的下拉输入框时,点击重置按钮后输入框无法回显数据的问题,具有一定的参考价值,感兴趣的... 目录描述原因问题重现解决方案方法一方法二总结描述第一次进入页面,不做任何操作,点击重置按钮,再进行下

解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题

《解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题》本文主要讲述了在使用MyBatis和MyBatis-Plus时遇到的绑定异常... 目录myBATis-plus-boot-starpythonter与mybatis-spring-b

mysql主从及遇到的问题解决

《mysql主从及遇到的问题解决》本文详细介绍了如何使用Docker配置MySQL主从复制,首先创建了两个文件夹并分别配置了`my.cnf`文件,通过执行脚本启动容器并配置好主从关系,文中还提到了一些... 目录mysql主从及遇到问题解决遇到的问题说明总结mysql主从及遇到问题解决1.基于mysql

电脑多久清理一次灰尘合? 合理清理电脑上灰尘的科普文

《电脑多久清理一次灰尘合?合理清理电脑上灰尘的科普文》聊起电脑清理灰尘这个话题,我可有不少话要说,你知道吗,电脑就像个勤劳的工人,每天不停地为我们服务,但时间一长,它也会“出汗”——也就是积累灰尘,... 灰尘的堆积几乎是所有电脑用户面临的问题。无论你的房间有多干净,或者你的电脑是否安装了灰尘过滤器,灰尘都

如何测试计算机的内存是否存在问题? 判断电脑内存故障的多种方法

《如何测试计算机的内存是否存在问题?判断电脑内存故障的多种方法》内存是电脑中非常重要的组件之一,如果内存出现故障,可能会导致电脑出现各种问题,如蓝屏、死机、程序崩溃等,如何判断内存是否出现故障呢?下... 如果你的电脑是崩溃、冻结还是不稳定,那么它的内存可能有问题。要进行检查,你可以使用Windows 11

如何安装HWE内核? Ubuntu安装hwe内核解决硬件太新的问题

《如何安装HWE内核?Ubuntu安装hwe内核解决硬件太新的问题》今天的主角就是hwe内核(hardwareenablementkernel),一般安装的Ubuntu都是初始内核,不能很好地支... 对于追求系统稳定性,又想充分利用最新硬件特性的 Ubuntu 用户来说,HWEXBQgUbdlna(Har

MAVEN3.9.x中301问题及解决方法

《MAVEN3.9.x中301问题及解决方法》本文主要介绍了使用MAVEN3.9.x中301问题及解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录01、背景02、现象03、分析原因04、解决方案及验证05、结语本文主要是针对“构建加速”需求交