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に戻ります。
コードは一見問題ないようですが、試してみてください。
ロックを解除するなら、その前に必ずロックをかけて、ロックの論理を見てみましょう。
錠をかける
ここをロックするロジックといえば、コードの中には二つの実現方法があります。
コード例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);
}

コード例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);
}

実はそれらの違いは前者がRedisTemplateを使っていますが、後者はStringRedisTemplateを使っています。
Q:ちょっと待ってください。なぜ二つのtemplateがありますか?
A:押して言いました。私が掘った穴です。RedisTemplateは私が加えたものです。今思い出しても、当初はなぜこのようにしたのか分かりませんでした。本当に頭が一時的にパンクしたかもしれません。
まずこの二つの方法をテストします。
テストします
それぞれロックをかけます。ここで、lock 01はk 1とv 1、lock 02はk 2とv 2です。
k 1、k 2の値をそれぞれ見てください。


v 1にはダブルクォーテーションがありますが、v 2にはありません。
推測は序列化されるべき問題です。Redisの配置を見てみますか?
RedisTemplateの設定
ロックはそこで見られますが、k 1はRedis Templateを使っています。k 2は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;
 }
}

StringRedis Templateの構成は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:Spring Bootバージョンは2.1.13 RELEASEです。
ポイントを入れてSteringRedis Templateを見てください。

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);
 // ...
}

StringRedis TemplateのkeyとvalueはデフォルトではStringRedis Serializer(Standard Charets.UTF_)を使っていることが見られます。8)序列化を行う。
RedisTemplateのkeyはStringRedis Serializerを使っています。valueはJackson 2 JsonRedis Serializerを使っています。
ここでは基本的に問題点に位置づけられます。つまりRedis TemplateのvalueプログレッシブとStringRedis Templateは一致しません。
もし同じに変更すればいいですか?検証してみます。
検証推論
RedisTemplateのvalueプログレッシブ方式をStringRedis Serializerに変更する:

@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;
 }
}

二つのロックロジックを呼び出して、k 1、k 2の値を見てください。


v 1のダブルクォーテーションがなくなり、ロックを解除するサービスも正常に削除されました。
はい、ここの問題です。
両者のソースコードの序列化については、興味のあるたらい友達が研究を続けてもいいです。ここではもう深く検討しません。
結び目
本論文で出会ったこの問題は、主に異なるRedisTemplateを使用してロックとリリースロックをかけていますが、この二つのtemplateは異なる順序化方式を使用しています。最終的にはまだ順序化による問題です。
最初は本当に軽率でした。しかもまだ予測できませんでした。
生産環境については、慎重かつ慎重に:深淵に臨んで、薄氷を踏むようである。
この記事では、Redisの分散式のロックについての序文を紹介します。Redisの分散式のロックに関する問題の内容は以前の文章を検索したり、下記の関連記事を見たりしてください。これからもよろしくお願いします。