雨翔河
首页
列表
关于
RedisTemplate 使用 setIfAbsent 做分布式锁出现返回值为 null 的问题
2022-10-27 13:57
> 问题背景:在实现一个分布式锁的时候经常会使用redis来做,而java的spring项目较常见的就是使用`RedisTemplate`来操作redis,使用 `redisTemplate.opsForValue().setIfAbsent()`来做一个分布式锁,但是在获取锁的时候,不管什么情况返回值都是null,使得不确定是否获得了锁。 这是某项目A从不知名地方拷贝过来的获取锁的代码: ``` public boolean lock(String lockKey, String lockValue){ return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(LOCK_EXPIRE_SECONDS))); } ``` 这代码这么看是没有什么大问题的,`setIfAbsent`,如果key不存在则设置值并且返回true,如果key存在则不设置值并且返回false。 实际上项目A运行起来的时候发现不管怎么调用`lock`方法,返回的都是false,简单查了一下发现不管怎么调用,`setIfAbsent`的返回值都是null,所以导致了lock方法返回了false。 但是在项目B也是这一段代码,调用结果却符合预期。 看了下`setIfAbsent`方法的官方注解: ``` /** * Set {@code key} to hold the string {@code value} and expiration {@code timeout} if {@code key} is absent. * * @param key must not be {@literal null}. * @param value must not be {@literal null}. * @param timeout must not be {@literal null}. * @return {@literal null} when used in pipeline / transaction. * @throws IllegalArgumentException if either {@code key}, {@code value} or {@code timeout} is not present. * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a> * @since 2.1 */ @Nullable default Boolean setIfAbsent(K key, V value, Duration timeout) { Assert.notNull(timeout, "Timeout must not be null!"); if (TimeoutUtils.hasMillis(timeout)) { return setIfAbsent(key, value, timeout.toMillis(), TimeUnit.MILLISECONDS); } return setIfAbsent(key, value, timeout.getSeconds(), TimeUnit.SECONDS); } ``` 也就是说项目A使用了`pipeline`或者是事物,导致了`setIfAbsent`返回了null值,但是很奇怪,都是RedisTemplate怎么会不一样呢。 那大概率就是两个项目引入的redis实际操作实现的包不一样,spring只是在外做了一层封装。 果然,项目A使用的是Redisson,项目B使用的是Lettuce。 首先可以排除是事物的影响,因为没有使用事务,那么我们看下spring的RedisTemplate是怎么判断是否使用pipeline。 ![](https://cdn.yuxianghe.net/image/blog/88-1.png) 都可以看到是通过`isPipelined `方法来判断是否开启pipeline执行。 那么我们找到关于Redisson的`isPipelined`的实现是怎么样的,它的默认值是什么。 ``` ...... org.redisson.spring.data.connection.RedissonConnection ...... @Override public boolean isPipelined() { if (executorService instanceof CommandBatchService) { CommandBatchService es = (CommandBatchService) executorService; return es.getOptions().getExecutionMode() == ExecutionMode.IN_MEMORY || es.getOptions().getExecutionMode() == ExecutionMode.IN_MEMORY_ATOMIC; } return false; } ``` 在不做设置的情况下,Redisson对于`getExecutionMode()`方法的类实现里有对这个值的默认设置: ``` org.redisson.api.BatchOptions ...... private ExecutionMode executionMode = ExecutionMode.IN_MEMORY; ...... public ExecutionMode getExecutionMode() { return executionMode; } ``` 也就是说默认值是`ExecutionMode.IN_MEMORY`,那么Redisson默认是开启这个pipeline的。 再看下关于Lettuce的isPipelined实现: ``` org.springframework.data.redis.connection.lettuce.LettuceConnection private boolean isPipelined = false; ``` spring对`LettuceConnection`的实现里的isPipelined值直接默认就是false,所以默认是关闭pipeline的。 这也就是为什么RedisTemplate使用`setIfAbsent`时候出现返回值为null的问题,假设如果是遇到了要更换组件,比如将Lettuce换成了Redisson,那么这个坑会留的特别大。 很多人喜欢使用包装了一层的东西来操作DB,包括但不限于各种ORM,我一直是持反对意见的,除非影响发工资,否则抵制ORM,我还是喜欢简单一点原生一点,大道至简。 另外: 有很多搜索到的文章说到RedisTemplate使用setIfAbsent做分布式锁出现返回值为null的问题没有说到点上,这个才是正确的答案。
类型:工作
标签:spring,RedisTemplate
Copyright © 雨翔河
我与我周旋久
独孤影
开源实验室