30w+数据使用RedisTemplate的pipeline空指针NullPointerException异常分析

为了实现布控预警的业务需求,在flink流式处理时查询redis进行比对,然后方案是放在redis里的数据格式为Set,key使用的直接是布控对象的值,比如身份证号、车牌号、手机号、imsi码等等,然后value的值为该布控对象所属的任务ID,之所以是这样设计,是因为可能有多个任务都设置了该布控对象,这时候比对到了该布控对象,会根据任务id查询对应的策略进行不同的业务处理。
目前一个任务的布控对象的数据量批量插入20w+的数据到redis中,数据量比较大,所以用到了pipe,在使用过程中发生了错误,发生了NullPointerException异常,进行了排查记录如下

首先debug代码,伪代码就是

redisTemplate.executePipeLined(connection -> {
    objectives.parallelStream().forEach(obj -> {
        connection.setCommands().sAdd(rawKey,rawValue);
    });
})

objectives是所有布控对象,这里我用到了并行流(后面我改成串行就没报错了,原因后面讲),代码是没啥问题的(),然后报错地方是执行完上述逻辑后,执行connection.closePipeLine()里的result.getResultHolder()该result是null,发现了异常,为啥list元素里会是空的呢,原来lettuce的pipe是初始化一个arraylist进行存储的,而我外面是一个并行流,相当于是并发调用connection.setCommands().sAdd()->pipe.add(),是这里发生了并发操作list的错误导致的。我自己写了一个测试用例,并行流调用arraylist.add,然后去遍历里面的元素,也是会报元素为空导致的NullPointerException

没想到啊,虽然lettuce底层是共享一个链接,使用的是netty的异步模型,不过也防止不了开发人员在外面是并发操作这个命令啊,lettuce是线程安全的库,但是这个LettuceConnection是spring这边实现的,结果来这一出...bug蛮多的,之前用spring-data-jpa那块分页也有点问题,模块太多了,依赖多个库之后配置多了可能spring的开发人员都弄不清关系了,给他们提了一个bug
https://github.com/spring-projects/spring-data-redis/issues/2653

接上面,我在调试的时候,pipe里有些返回值error是command timed out after 10 seconds,这就是原因,执行redis命令超时了,为啥会超时呢,由于我使用的是Lettuce的redis客户端库(最后发现就是这个库的原因),我开始自己调试,不调试不要紧,一调试震惊了,我在上面的forEach循环里打断点,结果发现执行一个sAdd就会立即把值flush推到redis服务器,马上就生效了。这说明lettuce的pipe根本就是个摆设,于是进去sAdd()看了一下,在DefaultEndpoint.write()方法里有个autoFlushCommands为true,所以其实每次sAdd()都会直接flush到服务器而不是添加到缓冲区buffer。
这里不得不说一下原因了,lettuce底层使用的是netty,是一个异步非阻塞(通过Future-Listener机制实现异步事件,网络IO使用的是同步非阻塞IO,即NIO)的线程和请求/响应模型(一般情况是不需要禁用自动刷新的,可见官网说明),所以我们才会在上面报command timed out after 10 seconds时没抛出来这个,因为使用的是netty的future每个请求都是异步的。这个错误是lettuce抛出来的,因为netty的异步模型,所以lettuce会给每个command(Future)设置一个ScheduleFuture定时任务,时间为timeout,里面的逻辑是如果command到时间了还没isDone()就会设置一个超时异常。

后面看了官网文档,其实建议我们用默认的自动flush就行了,我于是做了下对比,先把超时时间改大点,不让抛异常,然后比较性能,并行流因为有bug,所以这里我用反射把pipe的List实现arraylist改成了线程安全的list,并进行测试

我的布控对象测试数据解析后大概key有28w

实现方式 耗时 线程安全的list
并行流关闭自动flush 平均10.08/s
并行流开启自动flush 平均9.16/s
串行流关闭自动flush 平均15.36/s 平均16.20/s
串行流开启自动flush 平均6.74/s 平均8.06/s

可以看到官网推荐的自动每次执行就flush的性能更高些

lettuce设置timeout超时时间,spring.redis.timeout,我之前设置的是10s,可以自行调整
lettuce设置pipe的禁用自动刷新,这里需要重新构造LettuceConnectionFactory,然后设置PipelingFlushPolicy

我在官网找到了说明https://lettuce.io/core/release/reference/#_pipelining_and_co...
里面也说明了pipe默认每个命令在发出后都写入传输的原因,有兴趣的可以看看

你可能感兴趣的:(30w+数据使用RedisTemplate的pipeline空指针NullPointerException异常分析)