场景描述

最近使用 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]
thenreturn redis.call('del',KEYS[1])
elsereturn 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 {@Beanpublic 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@ConditionalOnMissingBeanpublic 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 {@Beanpublic 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 使用了不同的序列化方式,最终还是序列化带来的问题。

当初真是草率了,而且一时还没测出来……

对于生产环境,还是要慎之又慎:如临深渊,如履薄冰。

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

Redis 分布式锁遇到的序列化问题相关推荐

  1. redis setnx 分布式锁_手写Redis分布式锁

    分布式锁使用场景 现在的系统都是集群部署,每个服务都不是单节点的了.比如库存服务,可能部署到3台机器上分别命名为节点1,节点2,节点3.库存服务需要扣减库存,扣减库存肯定需要锁吧,如果使用Lock或者 ...

  2. 使用RedisTemplate实现Redis分布式锁出现的一些列问题(避坑)

    点击关注公众号,实用技术文章及时了解 来源:blog.csdn.net/lmx125254/ article/details/89604638 自己的项目因为会一直抓取某些信息,但是本地会和线上经常一 ...

  3. 从青铜到王者,带你完成Redis分布式锁的实现和优化

    0.分布式锁的常见面试题 Redis除了拿来做缓存,你还见过基于Redis的什么用法? Redis做分布式锁的时候有需要注意的问题? 如果是Redis是单点部署的,会带来什么问题? 那你准备怎么解决单 ...

  4. Redis分布式锁(图解 - 秒懂 - 史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  5. Redis分布式锁详解

    Redis分布式锁详解 1. 分布式所概述 1.1 分布式锁 2. 缓存数据库Redis 2.1 redis简介 2.2 Springboot整合Redis两种方式 3. 实现验证 3.1 环境准备 ...

  6. 电商项目实战之缓存与Redis分布式锁

    电商项目实战之缓存与Redis分布式锁 缓存失效 缓存穿透 缓存雪崩 缓存击穿 分布式缓存 分布式锁 SpringBoot整合Redisson实现分布式锁 实现过程 缓存和数据库一致性 场景分析 解决 ...

  7. Redis实战 - 04 Redis 分布式锁应用之抢购代金券

    文章目录 1. 数据库表结构 1. 代金券表 2. 抢购活动表 3. 订单表 2. 秒杀场景的解决方案 3. 创建秒杀服务 ms-seckill 4. 代金券抢购功能开发 - 关系型数据库实现 1. ...

  8. redis分布式锁 在集群模式下如何实现_收藏慢慢看系列:简洁实用的Redis分布式锁用法...

    在微服务中很多情况下需要使用到分布式锁功能,而目前比较常见的方案是通过Redis来实现分布式锁,网上关于分布式锁的实现方式有很多,早期主要是基于Redisson等客户端,但在Spring Boot2. ...

  9. 快来学习Redis 分布式锁的背后原理

    以前在学校做小项目的时候,用到Redis,基本也只是用来当作缓存.可阿粉在工作中发现,Redis在生产中并不只是当作缓存这么简单.在阿粉接触到的项目中,Redis起到了一个分布式锁的作用,具体情况是这 ...

最新文章

  1. python dataframe 计算上下两行的差值_用Python进行数据清洗!
  2. Android Studio 简介
  3. 微信小程序code 换取 session_key
  4. 编译问题 文件查找失败: ‘vant‘
  5. 封装javascript分页插件——可以使用的测试版(β版)
  6. python3 set_python3.x 基础三:set集合
  7. Google 的服务,你用了那些?
  8. [Vue.js] 路由 -- 前端路由
  9. Java基础——数组应用之StringBuilder类和StringBuffer类
  10. atitit.软件gui按钮and面板---os区-----软链接,快捷方式
  11. 有关Unity编辑器
  12. js word 预览_个人电子简历模板在线编辑,大学生简历模板免费下载word 文档
  13. 我爱淘冲刺阶段站立会议每天任务3
  14. Qt5.9生成一个Adroid的apk应用实例
  15. 使用Fiddler快速保存微信视频号上的视频
  16. Snipaste——一款强大又实用的截图工具
  17. OmniPeek-20180725-Error 2502/Error 2053
  18. oracle 物料属性批次过期,系列之五:ORACLE EBS 系统主数据管理(C)
  19. ROsalind 014 Finding a Shared Motif
  20. 笨方法学Python—ex43:基本的面向对象分析和设计

热门文章

  1. 季枫老师java全集_PHP冒泡排序,摘取季枫老师视频的冒泡排序内容!
  2. php概率计算_替你总结一份MIT计算机课程
  3. python内建函数调用,Python 内建函数
  4. java string类型详解_Java字符串类型详解
  5. (王道408考研数据结构)第八章排序-第三节2:堆与堆排序
  6. libevent的vs2013的源码工程 以及两个demo地址
  7. 全面介绍Windows内存管理机制及C++内存分配实例(二):内存状态查询
  8. JS 表单、表单验证(表单判断、get、post、submit、validity、checkValidity)
  9. 汇编语言:汇编程序的hello world程序(实验9根据材料编程)
  10. [bzoj1191][HNOI2006]超级英雄Hero