使用RedisAtomicInteger計數(shù)出現(xiàn)少計問題及解決
RedisAtomicInteger計數(shù)出現(xiàn)少計
最近工作中遇到了這樣一個場景
同一個外部單號生成了多張出庫單,等待所有相關(guān)的出庫單都出庫成功后回復成功消息外部系統(tǒng)調(diào)用方。因為是分布式布系統(tǒng),我使用了RedisAtomicInteger計數(shù)器來判斷出庫單是否全部完成,數(shù)量達成時回復成功消息給外部系統(tǒng)調(diào)用方。
在本地測試和測試環(huán)境測試時都沒有發(fā)現(xiàn)問題,到了生產(chǎn)環(huán)境后,發(fā)現(xiàn)偶爾出現(xiàn)所有出庫單都已經(jīng)出庫,但沒有回復消息給調(diào)用方,如:出庫單15張,但計數(shù)器只有14。
分析
開始以為是有單據(jù)漏計算了,通過日志分析,發(fā)現(xiàn)所有的出庫單都統(tǒng)計進去了。
然后通過增加打開調(diào)試日志,發(fā)現(xiàn)最開始的2張出庫單統(tǒng)計后的值都為1,少了1個。
原因
redis的increment是原子性,但new RedisAtomicInteger時會調(diào)用set方法來設置初始值,set方法是可以被后面的方法覆蓋的。
edisAtomicInteger redisAtomicInt = new RedisAtomicInteger(countKey, redisTemplate.getConnectionFactory()); ?? // spring-data-redis-1.8.13原碼 public RedisAtomicInteger(String redisCounter, RedisConnectionFactory factory) { ?? ??? ?this(redisCounter, factory, null); ?? ?} ?? private RedisAtomicInteger(String redisCounter, RedisConnectionFactory factory, Integer initialValue) { ?? ??? ?RedisTemplate<String, Integer> redisTemplate = new RedisTemplate<String, Integer>(); ?? ??? ?redisTemplate.setKeySerializer(new StringRedisSerializer()); ?? ??? ?redisTemplate.setValueSerializer(new GenericToStringSerializer<Integer>(Integer.class)); ?? ??? ?redisTemplate.setExposeConnection(true); ?? ??? ?redisTemplate.setConnectionFactory(factory); ?? ??? ?redisTemplate.afterPropertiesSet(); ? ?? ??? ?this.key = redisCounter; ?? ??? ?this.generalOps = redisTemplate; ?? ??? ?this.operations = generalOps.opsForValue(); ? ?? ??? ?if (initialValue == null) { ?? ??? ??? ?if (this.operations.get(redisCounter) == null) { ?? ??? ??? ??? ?set(0); ?? ??? ??? ?} ?? ??? ?} else { ?? ??? ??? ?set(initialValue); ?? ??? ?} ?? ?}
解決方法
網(wǎng)上看到的都是加業(yè)務鎖或升級spring-data-redis版本。
但老項目升級spring-data-redis版本可能會引起兼容性問題,加業(yè)務鎖又增加了代碼復雜度。
那有沒有更簡單方法呢,有。竟然是set方法導致的值覆蓋,那就不走set方法就可以了。
增加下面一行代碼解決問題
// Fixed bug 前幾個數(shù)累計重復問題 redisTemplate.opsForValue().setIfAbsent(countKey, 0);
使用RedisAtomicInteger中間遇到的問題
RedisAtomicInteger是springdata中在redis的基礎上實現(xiàn)的原子計數(shù)器,在以下maven依賴包中:
<groupId>org.springframework.data</groupId> ? ?? <artifactId>spring-data-redis</artifactId>?
當使用RedisAtomicInteger(String redisCounter, RedisOperations<String, Integer> template,...)函數(shù)構(gòu)建實例的情況下,在使用INCR或者DECR時,會遇到ERR value is not an integer or out of range錯誤,顯示操作的數(shù)據(jù)不是一個整數(shù)或者超出范圍。
參考redis命令說明我們知道incr對操作值的要求
這是一個針對字符串的操作,因為 Redis 沒有專用的整數(shù)類型,所以 key 內(nèi)儲存的字符串被解釋為十進制 64 位有符號整數(shù)來執(zhí)行 INCR 操作。如果值包含錯誤的類型,或字符串類型的值不能表示為數(shù)字,那么返回一個錯誤。
從redis實例中查看該key的value,會發(fā)現(xiàn)結(jié)果類似這樣:
"\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01"
原因在于value使用的序列化方式是JdkSerializationRedisSerializer,這和INCR命令對結(jié)果的要求是違背的。
該使用哪種序列化方式把value放進去呢?按照INCR命令對結(jié)果的要求,最容易想到StringRedisSerializer,但經(jīng)過嘗試這也行不通
會報java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String。
如果看過RedisAtomicInteger的源碼,在private RedisAtomicInteger(String redisCounter, RedisConnectionFactory factory, Integer initialValue)中會發(fā)現(xiàn)方法內(nèi)部創(chuàng)建了RedisTemplate實例,對value設置的序列化方式是GenericToStringSerializer。
該序列化內(nèi)部使用spring core包下的
org.springframework.core.convert.support.DefaultConversionService作為默認的對象和字符串的轉(zhuǎn)換方式,主要為了滿足大多數(shù)環(huán)境的要求。
至此,我們終于知道了錯誤的根本原因,構(gòu)造RedisAtomicInteger時傳入的redisTemplate是有問題的,value的默認序列化方式不滿足RedisAtomicInteger的需要。那么問題也迎刃而解,將GenericToStringSerializer作為redisTemplate的value序列化方式。
這樣雖然解決了問題,但很麻煩,很可能為了RedisAtomicInteger的要求需要再創(chuàng)建一個redisTemplate,簡直不能忍受。再看RedisAtomicInteger的源碼,發(fā)現(xiàn)構(gòu)造函數(shù)除了可以用redisTemplate,還可以用RedisConnectionFactory,嘗試之后,完美解決。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redisson實現(xiàn)分布式鎖、鎖續(xù)約的案例
這篇文章主要介紹了Redisson如何實現(xiàn)分布式鎖、鎖續(xù)約,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03阿里云官方Redis開發(fā)規(guī)范總結(jié)
本文主要介紹了阿里云官方Redis開發(fā)規(guī)范總結(jié),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-08-08阿里云服務器安裝配置redis的方法并且加入到開機啟動(推薦)
這篇文章主要介紹了阿里云服務器安裝配置redis并且加入到開機啟動,需要的朋友可以參考下2017-12-12redis+mysql+quartz 一種紅包發(fā)送功能的實現(xiàn)
這篇文章主要介紹了redis+mysql+quartz 一種紅包發(fā)送功能的實現(xiàn)的相關(guān)資料,需要的朋友可以參考下2017-01-01