共计 3047 个字符,预计需要花费 8 分钟才能阅读完成。
导读 | 这篇文章主要介绍了使用 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 是 springdata 中在 redis 的基础上实现的原子计数器,在以下 maven 依赖包中:
org.springframework.data
spring-data-redis
当使用 RedisAtomicInteger(String redisCounter, RedisOperations
这是一个针对字符串的操作,因为 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,尝试之后,完美解决。