当前位置: 首页 > 科技观察

Redis分布式锁遇到的序列化问题_0

时间:2023-03-12 04:38:07 科技观察

场景描述最近Redis遇到了类似分布式锁的场景。对比Redis实现分布式锁,释放锁失败,即无法删除缓存。又踩了一个Redis的坑……这是什么情况,怎么查看?本文主要对此进行评述。排查问题既然释放锁有问题,那我们先来看一下释放锁的代码。释放锁释放锁使用Lua脚本,代码逻辑和Lua脚本如下:释放锁示例代码publicObjectrelease(Stringkey,Stringvalue){ObjectexistedValue=stringRedisTemplate.opsForValue().get(key);log.info("key:{},value:{},redis旧值:{}",key,value,existedValue);DefaultRedisScriptredisScript=newDefaultRedisScript<>(COMPARE_AND_DELETE,Long.class);returnstringRedisTemplate.execute(redisScript,Collections.singletonList(key),value);}用于释放锁的lua脚本ifredis.call('get',KEYS[1])==ARGV[1]thenreturnredis.call('del',KEYS[1])elsereturn0end;在删除脚本中,会先获取Rediskey的旧值,与输入参数值进行比较,相等时才删除。如果释放成功,即成功删除Redis缓存,则返回值为1,否则返回值为0。乍一看,代码似乎没有问题。试试看?但是既然需要释放锁,那么就必须在这之前加锁。我们来看看加锁的逻辑。Locking说到加锁的逻辑,代码中有两种实现方式:示例代码一publicObjectlock01(Stringkey,Stringvalue){log.info("lock01,key={},value={}",key,value);returnredisTemplate.opsForValue().setIfAbsent(key,value,LOCKED_TIME,TimeUnit.SECONDS);}示例代码2publicObjectlock02(Stringkey,Stringvalue){log.info("lock02,key={},value={}",key,value);returnstringRedisTemplate.opsForValue().setIfAbsent(key,value,LOCKED_TIME,TimeUnit.SECONDS);}其实它们的区别在于前者使用的是RedisTemplate,后者使用的是StringRedisTemplate。问:等等……为什么有两个模板??A:我说了,我挖坑了,我加了RedisTemplate……现在想想,我想不通当初为什么要这么做。也许是抽搐了。.先测试这两种方法?分别使用两种方式加锁进行测试,其中:lock01为k1和v1,lock02为k2和v2。分别看k1和k2的值(使用工具:RDM、RedisDesktopManager):可以看到v1有双引号,v2没有。猜测应该是序列化问题。看看Redis的配置?RedisTemplate配置已锁定。可以看到k1使用的是RedisTemplate,k2是StringRedisTemplate。两种配置有什么区别?RedisTemplate的配置是自定义的,如下:@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)publicclassRedisConfig{@BeanpublicRedisTemplateredisTemplate(RedisConnectionFactoryredisConnectionFactory){RedisTemplateredisTemplate=newRedisTemplate<>();redisTemplate.setConnectionFactory(redisConnection/工厂使用);Jackson2JsonRedisSerialize替换默认序列化Jackson2JsonRedisSerializerjackson2JsonRedisSerializer=newJackson2JsonRedisSerializer<>(Object.class);ObjectMapperobjectMapper=newObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);//设置key,value序列化规则(尤其是value)redisTemplate.setKeySerializer(newStringRedisSerializer());redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();returnredisTemplate;}}StringRedisTemplate配置是SpringBoot默认的,即:@Configuration@ConditionalOnClass({RedisOperations.class})@EnableConfigurationProperties({RedisProperties.class})@Import({LettuceConnectionConfiguration.class,JedisConnectionConfiguration.class})publicclassRedisAutoConfiguration{publicRedisAutoConfiguration(){}@Bean@ConditionalOnMissingBeanpublicStringRedisTemplatestringRedisTemplate(RedisConnectionFactoryredisConnectionFactory)throwsUnknownHostException{String=newStringRedistemplate);setConnectionFactory(redisConnectionFactory);returntemplate;}}PS:SpringBoot版本是2.1.13.RELEASE点击进入StringRedisTemplate可以看到:publicclassStringRedisTemplateextendsRedisTemplate{publicStringRedisTemplate(){//注意这里的序列化设置setKeySerializer(RedisSerializer.string());setValueSerializer(RedisSerializer.string());setHashKeySerializer(RedisSerializer.string());setHashValueSerializer(RedisSerializer.string());}//...}注意序列化设置和继续跟进看看是什么:publicinterfaceRedisSerializer{staticRedisSerializerstring(){returnStringRedisSerializer.UTF_8;}}publicclassStringRedisSerializerimplementsRedisSerializer{publicstaticfinalStringRedisSerializerUTF_8=newStringRedisSerializer(UTF_8sets)可以看到.../StandardChar8sets;默认情况下,StringRedisTemplate的key和value默认使用StringRedisSerializer(StandardCharsets.UTF_8)进行序列化,而RedisTemplate的key使用StringRedisSerializer,value使用Jackson2JsonRedisSerializer进行序列化(至于为什么要这样,我这里没写).至此,我们基本可以定位到问题所在:RedisTemplate的值序列化与StringRedisTemplate不一致。改成一致就可以了吗?验证并尝试。验证推断,修改RedisTemplate的值序列化方式为StringRedisSerializer:@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)publicclassRedisConfig{@BeanpublicRedisTemplateredisTemplate(RedisConnectionFactoryredisConnectionFactory){RedisTemplateredisTemplate=newRedisTemplate<>()//...redisTemplate.setKeySerializer(newStringRedisSerializer());redisTemplate.setValueSerializer(newStringRedisSerializer());//...returnredisTemplate;}}再次调用两种加锁逻辑,看k1和k2的值:可以看到现在v1的双引号没有了,释放锁的服务也可以正常删除了。好吧,这就是问题所在。至于两者连载的源码,有兴趣的朋友可以继续研究,这里就不深入讨论了。总结本文遇到的问题主要是由于使用了不同的RedisTemplates加锁和释放锁,而这两个模板使用了不同的序列化方式,归根结底是序列化导致的问题。刚开始的时候真的是草草了事,好久没测试了。。。对于生产环境,还是要慎之又慎:如履薄冰。