Scene description

Recently, I encountered a similar distributed lock scenario using Redis, which is similar to the distributed lock implementation of Redis, that is, the lock release failure, that is, the cache cannot be deleted. Step on another Redis pit…

What is this situation, and how the investigation?

This paper mainly to do a review.

Troubleshoot problems

Since it is to release the lock problem, that first look at the lock release code.

Release the lock

Lua script is used to release locks. The code logic and Lua script are as follows:

  • Release lock sample code
public Object release(String key, String value) {
  Object existedValue = stringRedisTemplate.opsForValue().get(key);
  log.info(Key :{}, value:{}, redis old value:{}", key, value, existedValue);
  
  DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(COMPARE_AND_DELETE, Long.class);
  return stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value);
}
Copy the code
  • Lua script used to release locks
if redis.call('get',KEYS[1]) == ARGV[1]
then
    return redis.call('del',KEYS[1])
else
    return 0
end;
Copy the code

In the delete script, the old Redis key value is first obtained and compared with the value of the input parameter. The deletion will be performed only when the two values are equal.

The return value is 1 if the release was successful, that is, the Redis cache was deleted, and 0 if it failed.

At first glance the code seems to be fine. Test it out?

But if you want to release the lock, you must lock it before doing so. Let’s look at the logic of locking.

lock

When it comes to locking logic, there are two ways to do this in the code:

  • Example Code 1
public Object lock01(String key, String value) {
  log.info("lock01, key={}, value={}", key, value);
  return redisTemplate.opsForValue().setIfAbsent(key, value, LOCKED_TIME, TimeUnit.SECONDS);
}
Copy the code
  • Example Code 2
public Object lock02(String key, String value) {
  log.info("lock02, key={}, value={}", key, value);
  return stringRedisTemplate.opsForValue().setIfAbsent(key, value, LOCKED_TIME, TimeUnit.SECONDS);
}
Copy the code

The difference is that the former uses a RedisTemplate, while the latter uses a StringRedisTemplate.

Q: wait… Why are there two templates?

A: Let’s face it, I dug the hole and added the RedisTemplate… Now recall did not want to understand at the beginning why do so, may be the brain temporarily out of control.

Let’s test these two methods first, okay?

Test the

Lock01 is K1 and V1, and lock02 is K2 and v2.

Look at the values of K1 and k2 respectively (using tools: RDM, Redis Desktop Manager) :

You can see v1 has double quotes, v2 doesn’t.

Guess it should be serialization problem, see Redis configuration?

RedisTemplate configuration

You can see in the lock that K1 uses a RedisTemplate and K2 uses a StringRedisTemplate, so what’s the difference between the two configurations?

The configuration of RedisTemplate is customized as follows:

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
  @Bean
  public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(redisConnectionFactory);

    / / use Jackson2JsonRedisSerialize replace the default serialization
    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);

    // Set serialization rules for key and value (especially value)
    redisTemplate.setKeySerializer(new StringRedisSerializer());
    redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
    redisTemplate.afterPropertiesSet();

    returnredisTemplate; }}Copy the code

The configuration of StringRedisTemplate is SpringBoot default, i.e. :

@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
  public RedisAutoConfiguration(a) {}@Bean
  @ConditionalOnMissingBean
  public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    returntemplate; }}Copy the code

PS: The SpringBoot version is 2.1.13.RELEASE

I’m going to go ahead and say StringRedisTemplate:

public class StringRedisTemplate extends RedisTemplate<String.String> {
    
  public StringRedisTemplate(a) {
    // Notice the serialization Settings here
    setKeySerializer(RedisSerializer.string());
    setValueSerializer(RedisSerializer.string());
    setHashKeySerializer(RedisSerializer.string());
    setHashValueSerializer(RedisSerializer.string());
  }
  // ...
}
Copy the code

Take a look at the serialization Settings and follow along to see how they work:

public interface RedisSerializer<T> {
  static RedisSerializer<String> string(a) {
    returnStringRedisSerializer.UTF_8; }}Copy the code
public class StringRedisSerializer implements RedisSerializer<String> {
  public static final StringRedisSerializer UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);
    // ...
}
Copy the code

As you can see, StringRedisTemplate’s key and value are serialized by default with StringRedisSerializer(standardCharsets.utf_8).

And the key to use StringRedisSerializer RedisTemplate, the value is used Jackson2JsonRedisSerializer serialization (as to why this, here, it is not I write).

The value serialization of RedisTemplate is inconsistent with that of StringRedisTemplate.

Is it ok if you change it to consistent? Let’s verify that.

Verify the inference

Alter RedisTemplate value serialization to 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());

    // ...
    returnredisTemplate; }}Copy the code

Call two kinds of locking logic, look at k1, k2 values:

As you can see, the v1 double quotes are missing, and the lock release service can be deleted normally.

Well, that’s the problem here.

As for the source code of both serialization, interested friends can continue to study, here is no longer in-depth discussion.

summary

The problem encountered in this article was mainly due to the use of different redistemplates to lock and release locks, and the two templates used different serialization methods, which ultimately caused the serialization problem.

It was reckless, and it hadn’t been detected yet…

For the production environment, or to be extremely cautious: as in the abyss, treading on thin ice.