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