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

场景描述

最近使用 redis 遇到了一个类似分布式锁的场景,跟 redis 实现分布式锁类比一下,就是释放锁失败,也就是缓存删不掉。又踩了一个 redis 的坑……
这是什么个情况、又是怎样排查的呢?
本文主要对此做个复盘。

问题排查

既然是释放锁有问题,那就先看看释放锁的代码吧。

释放锁

释放锁使用了 lua 脚本,代码逻辑和 lua 脚本如下:

释放锁示例代码

public object release(string key, string value) {
 object existedvalue = stringredistemplate.opsforvalue().get(key);
 log.info("key:{}, value:{}, redis旧值:{}", key, value, existedvalue);
 
 defaultredisscript<long> redisscript = new defaultredisscript<>(compare_and_delete, long.class);
 return stringredistemplate.execute(redisscript, collections.singletonlist(key), value);
}

释放锁使用的 lua 脚本

if redis.call('get',keys[1]) == argv[1]
then
 return redis.call('del',keys[1])
else
 return 0
end;

删除脚本中,会先获取 redis key 的旧值,并与入参 value 比较,二者相等时才会删除。
如果释放成功,也就是 redis 缓存删除成功,返回值为 1,否则失败返回为 0。
乍一看代码似乎没啥问题,测一下试试?
不过既然要释放锁,在此之前肯定要加锁,先看看加锁的逻辑吧。

加锁

说到加锁这里的逻辑,代码里有两种实现方式:

示例代码一

public object lock01(string key, string value) {
 log.info("lock01, key={}, value={}", key, value);
 return redistemplate.opsforvalue().setifabsent(key, value, locked_time, timeunit.seconds);
}

示例代码二

public object lock02(string key, string value) {
 log.info("lock02, key={}, value={}", key, value);
 return stringredistemplate.opsforvalue().setifabsent(key, value, locked_time, timeunit.seconds);
}

其实它们的区别就在于前者使用了 redistemplate,而后者使用的是 stringredistemplate。

q: 等等……为什么会有两个 template??
a: 憋说了,是我挖的坑,redistemplate 是我加的……现在回想都没想明白当初为什么这样搞,可能真是脑子一时抽风了。

先测试一下这两个方法?

测试一下

使用两种方式分别加锁,其中:lock01 为 k1 和 v1,lock02 为 k2 和 v2。
分别看下 k1、k2 的值(使用工具:rdm, redis desktop manager):

可以看到 v1 是有双引号的,而 v2 没有。
猜测应该是序列化的问题,看看 redis 配置?

redistemplate 配置

加锁那里可以看到,k1 使用了 redistemplate,而 k2 是 stringredistemplate,它们两个的配置有什么区别呢?
其中 redistemplate 的配置是自定义的,如下:

@configuration
@autoconfigureafter(redisautoconfiguration.class)
public class redisconfig {
 @bean
 public redistemplate<string, object> redistemplate(redisconnectionfactory redisconnectionfactory) {
 redistemplate<string, object> redistemplate = new redistemplate<>();
 redistemplate.setconnectionfactory(redisconnectionfactory);

 // 使用 jackson2jsonredisserialize 替换默认序列化
 jackson2jsonredisserializer<object> jackson2jsonredisserializer
  = new jackson2jsonredisserializer<>(object.class);

 objectmapper objectmapper = new objectmapper();
 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(new stringredisserializer());
 redistemplate.setvalueserializer(jackson2jsonredisserializer);
 redistemplate.afterpropertiesset();

 return redistemplate;
 }
}

stringredistemplate 的配置是 springboot 默认的,即:

@configuration
@conditionalonclass({redisoperations.class})
@enableconfigurationproperties({redisproperties.class})
@import({lettuceconnectionconfiguration.class, jedisconnectionconfiguration.class})
public class redisautoconfiguration {
 public redisautoconfiguration() {
 }

 @bean
 @conditionalonmissingbean
 public stringredistemplate stringredistemplate(redisconnectionfactory redisconnectionfactory) throws unknownhostexception {
 stringredistemplate template = new stringredistemplate();
 template.setconnectionfactory(redisconnectionfactory);
 return template;
 }
}

ps: springboot 版本为 2.1.13.release

点进去 stringredistemplate 看下:

public class stringredistemplate extends redistemplate<string, string> {
 
 public stringredistemplate() {
 // 注意这里的序列化设置
 setkeyserializer(redisserializer.string());
 setvalueserializer(redisserializer.string());
 sethashkeyserializer(redisserializer.string());
 sethashvalueserializer(redisserializer.string());
 }
 // ...
}

注意下序列化设置,继续跟进,看到底是什么方式:

public interface redisserializer<t> {
 static redisserializer<string> string() {
 return stringredisserializer.utf_8;
 }
}
public class stringredisserializer implements redisserializer<string> {
 public static final stringredisserializer utf_8 = new stringredisserializer(standardcharsets.utf_8);
 // ...
}

可以看到,stringredistemplate 的 key 和 value 默认都是用 stringredisserializer(standardcharsets.utf_8) 进行序列化的。

而 redistemplate 的 key 使用 stringredisserializer,value 使用的是 jackson2jsonredisserializer 序列化(至于为什么用这个,这里就不是我写的了)。
到这里,基本可以定位到问题所在了:就是 redistemplate 的 value 序列化和 stringredistemplate 不一致。

如果改成一致就可以了吗?验证一下试试。

验证推论

把 redistemplate 的 value 序列化方式修改为 stringredisserializer:

@configuration
@autoconfigureafter(redisautoconfiguration.class)
public class redisconfig {
 @bean
 public redistemplate<string, object> redistemplate(redisconnectionfactory redisconnectionfactory) {
 redistemplate<string, object> redistemplate = new redistemplate<>();
  
 // ...

 redistemplate.setkeyserializer(new stringredisserializer());
 redistemplate.setvalueserializer(new stringredisserializer());

 // ...
 return redistemplate;
 }
}

再调用两种加锁逻辑,看下 k1、k2 的值:

可以看到,v1 的双引号没了,释放锁的服务也能正常删掉了。
嗯,就是这里的问题。
至于两者序列化的源码,有兴趣的盆友们可以继续研究,这里就不再深入探讨了。

小结

本文遇到的这个问题,主要是因为使用了不同的 redistemplate 来加锁和释放锁,而这两个 template 使用了不同的序列化方式,最终还是序列化带来的问题。
当初真是草率了,而且一时还没测出来……
对于生产环境,还是要慎之又慎:如临深渊,如履薄冰。

到此这篇关于redis 分布式锁遇到的序列化问题的文章就介绍到这了,更多相关redis 分布式锁遇到的序列化问题内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!

(0)
上一篇 2022年3月21日
下一篇 2022年3月21日

相关推荐