30w+數(shù)據(jù)使用RedisTemplate?pipeline空指針NullPointerException異常分析
正文
為了實現(xiàn)布控預警的業(yè)務(wù)需求,在flink流式處理時查詢redis進行比對,然后方案是放在redis里的數(shù)據(jù)格式為Set,key使用的直接是布控對象的值,比如身份證號、車牌號、手機號、imsi碼等等,然后value的值為該布控對象所屬的任務(wù)ID,之所以是這樣設(shè)計,是因為可能有多個任務(wù)都設(shè)置了該布控對象,這時候比對到了該布控對象,會根據(jù)任務(wù)id查詢對應(yīng)的策略進行不同的業(yè)務(wù)處理。
目前一個任務(wù)的布控對象的數(shù)據(jù)量批量插入20w+的數(shù)據(jù)到redis中,數(shù)據(jù)量比較大,所以用到了pipe,在使用過程中發(fā)生了錯誤,發(fā)生了NullPointerException異常,進行了排查記錄如下
首先debug代碼
偽代碼就是
redisTemplate.executePipeLined(connection -> {
objectives.parallelStream().forEach(obj -> {
connection.setCommands().sAdd(rawKey,rawValue);
});
})objectives是所有布控對象,這里我用到了并行流(后面我改成串行就沒報錯了,原因后面講),代碼是沒啥問題的,然后報錯地方是執(zhí)行完上述邏輯后,執(zhí)行connection.closePipeLine()里的result.getResultHolder()該result是null,發(fā)現(xiàn)了異常,為啥list元素里會是空的呢,原來lettuce的pipe是初始化一個arraylist進行存儲的,而我外面是一個并行流,相當于是并發(fā)調(diào)用connection.setCommands().sAdd()->pipe.add(),是這里發(fā)生了并發(fā)操作list的錯誤導致的。
我自己寫了一個測試用例,并行流調(diào)用arraylist.add,然后去遍歷里面的元素,也是會報元素為空導致的NullPointerException
沒想到啊,雖然lettuce底層是共享一個鏈接,使用的是netty的異步模型,不過也防止不了開發(fā)人員在外面是并發(fā)操作這個命令啊,lettuce是線程安全的庫,但是這個LettuceConnection是spring這邊實現(xiàn)的,結(jié)果來這一出...bug蠻多的,之前用spring-data-jpa那塊分頁也有點問題,模塊太多了,依賴多個庫之后配置多了可能spring的開發(fā)人員都弄不清關(guān)系了,給他們提了一個bug
https://github.com/spring-projects/spring-data-redis/issues/2653
調(diào)試分析
接上面,我在調(diào)試的時候,pipe里有些返回值error是command timed out after 10 seconds,這就是原因,執(zhí)行redis命令超時了,為啥會超時呢,由于我使用的是Lettuce的redis客戶端庫(最后發(fā)現(xiàn)就是這個庫的原因),我開始自己調(diào)試,不調(diào)試不要緊,一調(diào)試震驚了,我在上面的forEach循環(huán)里打斷點,結(jié)果發(fā)現(xiàn)執(zhí)行一個sAdd就會立即把值flush推到redis服務(wù)器,馬上就生效了。
這說明lettuce的pipe根本就是個擺設(shè),于是進去sAdd()看了一下,在DefaultEndpoint.write()方法里有個autoFlushCommands為true,所以其實每次sAdd()都會直接flush到服務(wù)器而不是添加到緩沖區(qū)buffer。
這里不得不說一下原因了,lettuce底層使用的是netty,是一個異步非阻塞(通過Future-Listener機制實現(xiàn)異步事件,網(wǎng)絡(luò)IO使用的是同步非阻塞IO,即NIO)的線程和請求/響應(yīng)模型(一般情況是不需要禁用自動刷新的,可見官網(wǎng)說明),所以我們才會在上面報command timed out after 10 seconds時沒拋出來這個,因為使用的是netty的future每個請求都是異步的。
這個錯誤是lettuce拋出來的,因為netty的異步模型,所以lettuce會給每個command(Future)設(shè)置一個ScheduleFuture定時任務(wù),時間為timeout,里面的邏輯是如果command到時間了還沒isDone()就會設(shè)置一個超時異常。
后面看了官網(wǎng)文檔,其實建議我們用默認的自動flush就行了,我于是做了下對比,先把超時時間改大點,不讓拋異常,然后比較性能,并行流因為有bug,所以這里我用反射把pipe的List實現(xiàn)arraylist改成了線程安全的list,并進行測試
我的布控對象測試數(shù)據(jù)解析后大概key有28w
| 實現(xiàn)方式 | 耗時 | 線程安全的list |
|---|---|---|
| 并行流關(guān)閉自動flush | 平均10.08/s | |
| 并行流開啟自動flush | 平均9.16/s | |
| 串行流關(guān)閉自動flush | 平均15.36/s | 平均16.20/s |
| 串行流開啟自動flush | 平均6.74/s | 平均8.06/s |
可以看到官網(wǎng)推薦的自動每次執(zhí)行就flush的性能更高些
lettuce設(shè)置timeout超時時間,spring.redis.timeout,我之前設(shè)置的是10s,可以自行調(diào)整
lettuce設(shè)置pipe的禁用自動刷新,這里需要重新構(gòu)造LettuceConnectionFactory,然后設(shè)置PipelingFlushPolicy
我在官網(wǎng)找到了說明https://lettuce.io/core/release/reference/#_pipelining_and_co...里面也說明了pipe默認每個命令在發(fā)出后都寫入傳輸?shù)脑?,有興趣的可以看看
以上就是30w+數(shù)據(jù)使用RedisTemplate pipeline空指針NullPointerException異常分析的詳細內(nèi)容,更多關(guān)于RedisTemplate pipeline空指針異常的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot?aop里的@Pointcut()的配置方式
這篇文章主要介紹了springboot?aop里的@Pointcut()的配置方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
Spring boot配置多數(shù)據(jù)源代碼實例
這篇文章主要介紹了Spring boot配置多數(shù)據(jù)源代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-07-07
Spring Cloud詳解實現(xiàn)聲明式微服務(wù)調(diào)用OpenFeign方法
這篇文章主要介紹了Spring Cloud實現(xiàn)聲明式微服務(wù)調(diào)用OpenFeign方法,OpenFeign 是 Spring Cloud 家族的一個成員, 它最核心的作用是為 HTTP 形式的 Rest API 提供了非常簡潔高效的 RPC 調(diào)用方式,希望對大家有所幫助。一起跟隨小編過來看看吧2022-07-07
MybatisPlus自定義Sql實現(xiàn)多表查詢的示例
這篇文章主要介紹了MybatisPlus自定義Sql實現(xiàn)多表查詢的示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-08-08
Hibernate用ThreadLocal模式(線程局部變量模式)管理Session
今天小編就為大家分享一篇關(guān)于Hibernate用ThreadLocal模式(線程局部變量模式)管理Session,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03
MyBatis中insert操作返回主鍵的實現(xiàn)方法
在使用MyBatis做持久層時,insert語句默認是不返回記錄的主鍵值,而是返回插入的記錄條數(shù)。這篇文章主要介紹了MyBatis中insert操作返回主鍵的方法,需要的朋友可以參考下2016-09-09

