阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

使用RedisAtomicInteger计数出现少计问题及解决方法

74次阅读
没有评论

共计 3047 个字符,预计需要花费 8 分钟才能阅读完成。

导读 这篇文章主要介绍了使用 RedisAtomicInteger 计数出现少计问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
RedisAtomicInteger 计数出现少计

最近工作中遇到了这样一个场景

同一个外部单号生成了多张出库单,等待所有相关的出库单都出库成功后回复成功消息外部系统调用方。因为是分布式布系统,我使用了 RedisAtomicInteger 计数器来判断出库单是否全部完成,数量达成时回复成功消息给外部系统调用方。

在本地测试和测试环境测试时都没有发现问题,到了生产环境后,发现偶尔出现所有出库单都已经出库,但没有回复消息给调用方,如:出库单 15 张,但计数器只有 14。

分析

开始以为是有单据漏计算了,通过日志分析,发现所有的出库单都统计进去了。

然后通过增加打开调试日志,发现最开始的 2 张出库单统计后的值都为 1,少了 1 个。

原因

redis 的 increment 是原子性,但 new RedisAtomicInteger 时会调用 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 redisTemplate = new RedisTemplate();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericToStringSerializer(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);
        }
    }
解决方法

网上看到的都是加业务锁或升级 spring-data-redis 版本。

但老项目升级 spring-data-redis 版本可能会引起兼容性问题,加业务锁又增加了代码复杂度。

那有没有更简单方法呢,有。竟然是 set 方法导致的值覆盖,那就不走 set 方法就可以了。

增加下面一行代码解决问题

// Fixed bug 前几个数累计重复问题
redisTemplate.opsForValue().setIfAbsent(countKey, 0);
使用 RedisAtomicInteger 中间遇到的问题

RedisAtomicInteger 是 springdata 中在 redis 的基础上实现的原子计数器,在以下 maven 依赖包中:

org.springframework.data     
spring-data-redis

当使用 RedisAtomicInteger(String redisCounter, RedisOperations template,…) 函数构建实例的情况下,在使用 INCR 或者 DECR 时,会遇到 ERR value is not an integer or out of range 错误,显示操作的数据不是一个整数或者超出范围。

参考 redis 命令说明我们知道 incr 对操作值的要求

这是一个针对字符串的操作,因为 Redis 没有专用的整数类型,所以 key 内储存的字符串被解释为十进制 64 位有符号整数来执行 INCR 操作。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

从 redis 实例中查看该 key 的 value, 会发现结果类似这样:

"\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 命令对结果的要求是违背的。

该使用哪种序列化方式把 value 放进去呢?按照 INCR 命令对结果的要求,最容易想到 StringRedisSerializer,但经过尝试这也行不通

 会报 java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String。

如果看过 RedisAtomicInteger 的源码,在 private RedisAtomicInteger(String redisCounter, RedisConnectionFactory factory, Integer initialValue) 中会发现方法内部创建了 RedisTemplate 实例,对 value 设置的序列化方式是 GenericToStringSerializer。

该序列化内部使用 spring core 包下的

org.springframework.core.convert.support.DefaultConversionService 作为默认的对象和字符串的转换方式,主要为了满足大多数环境的要求。

至此,我们终于知道了错误的根本原因,构造 RedisAtomicInteger 时传入的 redisTemplate 是有问题的,value 的默认序列化方式不满足 RedisAtomicInteger 的需要。那么问题也迎刃而解,将 GenericToStringSerializer 作为 redisTemplate 的 value 序列化方式。

这样虽然解决了问题,但很麻烦,很可能为了 RedisAtomicInteger 的要求需要再创建一个 redisTemplate,简直不能忍受。再看 RedisAtomicInteger 的源码,发现构造函数除了可以用 redisTemplate, 还可以用 RedisConnectionFactory,尝试之后,完美解决。

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-24发表,共计3047字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中