场景描述
最近使用 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!