Disruptor仅发布不消费源码分析和Disruptor特异异常处理
1、起因背景
博主练手写的并发项目订单服务压测到一定数据量的时候出现Disruptor仅发布消息并没有去消费,之后将jemeter停止压测并手动使用postman发送请求也是这种现象。 |
2、初步定位
于是博主第一时间去分析Disruptor消费的代码块以及消费的条件或者说由于什么样的原因可能会导致不消费。
消费源码如下:
核心方法代码:this.workHandler.onEvent(event)
分析一下这块代码的特性,它是一个死循环,但它也有退出的条件,就是2和3,1并不会退出死循环,只是让它符合条件才继续下面的操作。
3、细化定位
jemeter压测,同时观察数据库数据增长情况,在数据停止增长的时候停止jemeter运行。针对不消费候选条件1,我们使用postman发请求,发现程序并未执行到断点处,也就是不消费候选条件1对应的代码块。这时候,我们可以暂时判断这个run方法已经退出了,符合不消费候选条件2和3。由于退出的时候是在jemeter压测的时候发生的,我们将断点打到不消费候选条件2和3,并开启新一轮压测。终于~~找到的原因。
很明显这是数据入库的时候发生了主键重复的冲突。
但是为什么我的多线程异常捕获的时候是个空指针错误呢?
复测之后,发现这个是由于这个引用属性为空导致的。
综上所述,Disruptor的仅发布不消费的原因是执行run方法的线程先抛出主键重复的冲突异常,然后catch块捕获后由于this.exceptionHandler
属性应用为空抛出了没有吞掉而向上抛给jvm处理的空制针异常,这样的话这个执行run方法的线程的生命周期已经到destroy阶段了-执行run方法的线程没有吞掉。
4、提供解决方案
既然找到了原因,我们就反过来思考this.exceptionHandler
如果不为空呢,这样是不是就会吞掉。那么我们假使this.exceptionHandler
不为空,看看this.exceptionHandler.handleEventException(var9, nextSequence, event);
的具体实现。
4.1、框架默认提供两种极端的方案
默认提供两种:
- FatalExceptionHandler实现:
调用方式:com.lmax.disruptor.dsl.disruptor.handleExceptionsWith(new FatalExceptionHandler());
这种最终也会向上抛给jvm处理,导致run方法退出,后续不再消费,不可取。 |
- IgnoreExceptionHandler实现:
调用方式:com.lmax.disruptor.dsl.disruptor.handleExceptionsWith(new IgnoreExceptionHandler());
这种倒是可以不让run方法退出,但是我那些入库失败的数据怎么办呢,我又没有办法在源码中嵌入我自己的业务补偿逻辑。 |
4.2、框架提供的另一种兼容中和的方案(几乎符合所有的业务解决方案思路)
于是,博主发现了一个既可以在消费抛出异常的时候不退出run方法,又能记录失败数据进行后续补偿(其实上面两种极端的也是这个调用设置,只不过提供的两个ExceptionHandler是框架自带的)。
public void handleExceptionsWith(ExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
其中ExceptionHandler 是个接口
因此我们实现这个接口就可以了:
当我们实现了这个接口之后,在之前那个异常处理的地方会调用我们实现该接口的这个方法:
其中o就是发布的消息(失败的数据),我们可以在这里将我们失败的数据记录下来后续做补偿。
5、思考
- 开发测试阶段压测特定场景埋点输出的时候尽量针对性处理,我指的是结果易寻,因为在海量日志中寻找未免有些麻烦,就比如博主此次线程池定义的线程异常捕获仅仅是log一下,这其实是很难发现的,可以尝试将异常输出到一个文件等方法。
6、花絮
com.lmax.disruptor.dsl.disruptor.handleExceptionsWith(ExceptionHandler exceptionHandler)
设置不生效源码分析